From 56a12393f48daf0b78dbf7dca63e0824797ba30a Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Fri, 4 Oct 2019 17:15:39 -0700 Subject: [PATCH 1/8] Support RTL layout for audio waveforms --- .../Cells/AudioWaveformProgressView.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/AudioWaveformProgressView.swift b/Signal/src/ViewControllers/ConversationView/Cells/AudioWaveformProgressView.swift index 8ebe70c1a3..55908366dc 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/AudioWaveformProgressView.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/AudioWaveformProgressView.swift @@ -137,7 +137,10 @@ class AudioWaveformProgressView: UIView { playedShapeLayer.frame = layer.frame unplayedShapeLayer.frame = layer.frame - thumbImageView.center = CGPoint(x: sampleHMargin + (samplesWidth * value), y: layer.frame.center.y) + + var thumbXPos = sampleHMargin + (samplesWidth * value) + if CurrentAppContext().isRTL { thumbXPos = samplesWidth - thumbXPos } + thumbImageView.center = CGPoint(x: thumbXPos, y: layer.frame.center.y) defer { playedShapeLayer.path = playedBezierPath.cgPath @@ -161,10 +164,13 @@ class AudioWaveformProgressView: UIView { // Center the sample vertically. let yPos = frame.center.y - height / 2 + var xPos = CGFloat(x) * (sampleWidth + sampleSpacing) + sampleHMargin + if CurrentAppContext().isRTL { xPos = samplesWidth - xPos } + path.append( UIBezierPath( roundedRect: CGRect( - x: CGFloat(x) * (sampleWidth + sampleSpacing) + sampleHMargin, + x: xPos, y: yPos, width: sampleWidth, height: height From ea808e7bd8507b014e89996f64b49ab2872ba7a5 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Mon, 7 Oct 2019 13:54:26 -0700 Subject: [PATCH 2/8] Fix audio waveform for android originated voice notes --- .../Messages/Attachments/TSAttachmentStream.m | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index dcd26a80c9..8c2dce2d41 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -820,6 +820,36 @@ typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail); } } else { AVURLAsset *asset = [AVURLAsset assetWithURL:self.originalMediaURL]; + + // If the asset isn't readable, we may not be able to generate a waveform for this file + if (!asset.isReadable) { + // Android sends voice messages in a hacky m4a container that we can't process + // when it has the m4a extension. If we hint to the OS that it's an AAC file with + // the file extension, we can. This is pretty brittle and hopefully android will + // be able to fix the issue in the future in which case `isReadable` will become + // true and this path will no longer be hit. + if (self.isVoiceMessage && [self.originalFilePath hasSuffix:@"m4a"]) { + NSString *symlinkPath = [self.uniqueIdAttachmentFolder stringByAppendingString:@"/Voice-Memo.aac"]; + if (![NSFileManager.defaultManager fileExistsAtPath:symlinkPath]) { + [self ensureUniqueIdAttachmentFolder]; + NSError *error; + [[NSFileManager defaultManager] createSymbolicLinkAtPath:symlinkPath + withDestinationPath:self.originalFilePath + error:&error]; + if (error) { + OWSFailDebug(@"Failed to create voice memo symlink: %@", error); + return nil; + } + } + asset = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:symlinkPath]]; + } + } + + if (!asset.isReadable) { + OWSFailDebug(@"unexpectedly encountered unreadable audio file."); + return nil; + } + waveform = [[AudioWaveform alloc] initWithAsset:asset]; // Listen for sampling completion so we can cache the final waveform to disk. From 820deae78781a57097292002b179bd7b940f5569 Mon Sep 17 00:00:00 2001 From: Jim Gustafson Date: Sun, 25 Aug 2019 18:19:44 -0700 Subject: [PATCH 3/8] Integrate RingRTC This commit integrates RingRTC in to Signal-iOS. Only two source code files are affected, CallService.swift and PeerConnection.swift, as well as the Xcode project. Other code, related to the WebRTC protocol buffers, has been removed since it is now handled by RingRTC. RingRTC is provided as a framework named SignalRingRTC.framework. This works in tandem with WebRTC, and hence currently requires that as well, WebRTC.framework. To make integration easier, avoid a zombie repository on GitHub, and keep the integrity of older versions, we re-use the signal-webrtc-ios-artifacts repository. We simply add the new framework for SignalRingRTC in the Build directory. The Xcode project is adjusted to include the new SignalRingRTC framework. Please note that the WebRTC.framework is also modified for RingRTC, and is itself updated and part of the delivery. Both of the framework artifacts come from the ringrtc repository. The PeerConnectionClient.swift implementation is very different than the previous version. It now serves as a thin wrapper around the RingRTC CallConnection class. It provides some fundamental serialization and helps with overall stability, although it could be deprecated eventually. For the CallService, the basic callflow is changed. Previously, it would use promise chains to go through RTC negotiation until a message was created for signaling to the peer. Now, RingRTC itself handles this, and those promise chains are replaced with singular calls in to the CallConnection (via PeerConnectionClient), with asynchronous callbacks coming in the future for the signaling parts and other key notifications. In summary, other aspects of the integration on CallService include: - The removal of the call timeout and associated callConnectedPromise. Timeouts are handled within RingRTC. - For incoming calls, the 'backgroundTask' is now a variable in the CallData class and cleared on call connect or eventual deinit. - When receiving Ice candidates, the existing behavior of queueing them until the PeerConnectionClient is valid is maintained. - When sending Ice candidates, the existing behavior of queueing them so they are sent in batches in cadence with the actual sending speed is maintained. - All signaling handlers, to send messages such as offer, answer, etc., are handled asynchronously and in a consistent way, with call failures being issued for any negative result. --- Signal.xcodeproj/project.pbxproj | 21 +- .../Generated/OWSWebRTCDataProtos.pb.swift | 317 ---- Signal/src/Generated/WebRTCProto.swift | 392 ----- Signal/src/call/CallService.swift | 901 +++++----- Signal/src/call/PeerConnectionClient.swift | 1466 ++++------------- ThirdParty/WebRTC | 2 +- protobuf/Makefile | 12 - protobuf/OWSWebRtcDataProtos.proto | 37 - 8 files changed, 812 insertions(+), 2336 deletions(-) delete mode 100644 Signal/src/Generated/OWSWebRTCDataProtos.pb.swift delete mode 100644 Signal/src/Generated/WebRTCProto.swift delete mode 100644 protobuf/Makefile delete mode 100644 protobuf/OWSWebRtcDataProtos.proto diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 42cec7fb48..2589dbf3ec 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -286,8 +286,6 @@ 34C3C78F2040A4F70000134C /* sonarping.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 34C3C78E2040A4F70000134C /* sonarping.mp3 */; }; 34C3C7922040B0DD0000134C /* OWSAudioPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 34C3C7902040B0DC0000134C /* OWSAudioPlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34C3C7932040B0DD0000134C /* OWSAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C3C7912040B0DC0000134C /* OWSAudioPlayer.m */; }; - 34C4E2572118957600BEA353 /* OWSWebRTCDataProtos.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C4E2552118957600BEA353 /* OWSWebRTCDataProtos.pb.swift */; }; - 34C4E2582118957600BEA353 /* WebRTCProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C4E2562118957600BEA353 /* WebRTCProto.swift */; }; 34C6B0A91FA0E46F00D35993 /* test-gif.gif in Resources */ = {isa = PBXBuildFile; fileRef = 34C6B0A51FA0E46F00D35993 /* test-gif.gif */; }; 34C6B0AB1FA0E46F00D35993 /* test-mp3.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 34C6B0A71FA0E46F00D35993 /* test-mp3.mp3 */; }; 34C6B0AC1FA0E46F00D35993 /* test-mp4.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 34C6B0A81FA0E46F00D35993 /* test-mp4.mp4 */; }; @@ -340,6 +338,7 @@ 34EA69422194DE8000702471 /* MediaUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EA69412194DE7F00702471 /* MediaUploadView.swift */; }; 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; }; 34FDB29221FF986600A01202 /* UIView+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34FDB29121FF986600A01202 /* UIView+OWS.swift */; }; + 38AD4FAA2310B7E00038BA75 /* SignalRingRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38AD4FA92310B7E00038BA75 /* SignalRingRTC.framework */; }; 4503F1BE20470A5B00CEE724 /* classic-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */; }; 4503F1BF20470A5B00CEE724 /* classic.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BC20470A5B00CEE724 /* classic.aifc */; }; 4503F1C3204711D300CEE724 /* OWS107LegacySounds.m in Sources */ = {isa = PBXBuildFile; fileRef = 4503F1C1204711D200CEE724 /* OWS107LegacySounds.m */; }; @@ -1087,8 +1086,6 @@ 34C3C78E2040A4F70000134C /* sonarping.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = sonarping.mp3; path = Signal/AudioFiles/sonarping.mp3; sourceTree = SOURCE_ROOT; }; 34C3C7902040B0DC0000134C /* OWSAudioPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAudioPlayer.h; sourceTree = ""; }; 34C3C7912040B0DC0000134C /* OWSAudioPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAudioPlayer.m; sourceTree = ""; }; - 34C4E2552118957600BEA353 /* OWSWebRTCDataProtos.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSWebRTCDataProtos.pb.swift; sourceTree = ""; }; - 34C4E2562118957600BEA353 /* WebRTCProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebRTCProto.swift; sourceTree = ""; }; 34C6B0A51FA0E46F00D35993 /* test-gif.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = "test-gif.gif"; sourceTree = ""; }; 34C6B0A71FA0E46F00D35993 /* test-mp3.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "test-mp3.mp3"; sourceTree = ""; }; 34C6B0A81FA0E46F00D35993 /* test-mp4.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = "test-mp4.mp4"; sourceTree = ""; }; @@ -1176,6 +1173,7 @@ 34FDB29121FF986600A01202 /* UIView+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+OWS.swift"; sourceTree = ""; }; 399D8A7F461D7253DFFB91C5 /* Pods-SignalTests.testable release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.testable release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.testable release.xcconfig"; sourceTree = ""; }; 4224D4E5D7921F25823ECDCA /* Pods-SignalPerformanceTests.testable release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalPerformanceTests.testable release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalPerformanceTests/Pods-SignalPerformanceTests.testable release.xcconfig"; sourceTree = ""; }; + 38AD4FA92310B7E00038BA75 /* SignalRingRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SignalRingRTC.framework; path = ThirdParty/WebRTC/Build/SignalRingRTC.framework; sourceTree = ""; }; 435EAC2E5E22D3F087EB3192 /* Pods-SignalShareExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.app store release.xcconfig"; sourceTree = ""; }; 4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "classic-quiet.aifc"; sourceTree = ""; }; 4503F1BC20470A5B00CEE724 /* classic.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = classic.aifc; sourceTree = ""; }; @@ -1566,6 +1564,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 38AD4FAA2310B7E00038BA75 /* SignalRingRTC.framework in Frameworks */, 4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */, 455A16DD1F1FEA0000F86704 /* Metal.framework in Frameworks */, 455A16DE1F1FEA0000F86704 /* MetalKit.framework in Frameworks */, @@ -2219,15 +2218,6 @@ path = Signal/AudioFiles/ringtoneSounds; sourceTree = SOURCE_ROOT; }; - 34C4E2542118957600BEA353 /* Generated */ = { - isa = PBXGroup; - children = ( - 34C4E2552118957600BEA353 /* OWSWebRTCDataProtos.pb.swift */, - 34C4E2562118957600BEA353 /* WebRTCProto.swift */, - ); - path = Generated; - sourceTree = ""; - }; 34C6B0A41FA0E46F00D35993 /* Assets */ = { isa = PBXGroup; children = ( @@ -2616,7 +2606,6 @@ 76EB03C318170B33006006FC /* AppDelegate.m */, 76EB03FE18170B33006006FC /* call */, 76EB041118170B33006006FC /* environment */, - 34C4E2542118957600BEA353 /* Generated */, 45D231751DC7E8C50034FA89 /* Jobs */, 457F3AC01D14A0F700C51351 /* Models */, 76EB041D18170B33006006FC /* network */, @@ -2922,6 +2911,7 @@ D221A08C169C9E5E00537ABF /* Frameworks */ = { isa = PBXGroup; children = ( + 38AD4FA92310B7E00038BA75 /* SignalRingRTC.framework */, 3496955F21A2FC8100DCFE74 /* CloudKit.framework */, 4C9CA25C217E676900607C63 /* ZXingObjC.framework */, 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */, @@ -3549,6 +3539,7 @@ inputPaths = ( "$(SRCROOT)/ThirdParty/WebRTC/Build/WebRTC.framework", "$(SRCROOT)/ThirdParty/Carthage/Build/iOS/ZXingObjC.framework", + "$(SRCROOT)/ThirdParty/WebRTC/Build/SignalRingRTC.framework", ); name = "[Carthage] Copy Frameworks"; outputPaths = ( @@ -4049,7 +4040,6 @@ 4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */, 3434AE1C22AEDE7D002EE04E /* ViewOnceMessageViewController.swift in Sources */, 8835DE03230DEC6A00DC6B66 /* AddToBlockListViewController.swift in Sources */, - 34C4E2582118957600BEA353 /* WebRTCProto.swift in Sources */, 34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */, 4C8A6DFE22E54AFA00469AE7 /* MediaInteractiveDismiss.swift in Sources */, 452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */, @@ -4264,7 +4254,6 @@ 8809CE8A22F93C2200D38867 /* RecentPhotoCollectionView.swift in Sources */, 3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */, 340FC8B3204DAC8D007AEB0F /* AppSettingsViewController.m in Sources */, - 34C4E2572118957600BEA353 /* OWSWebRTCDataProtos.pb.swift in Sources */, 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */, 45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */, 34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */, diff --git a/Signal/src/Generated/OWSWebRTCDataProtos.pb.swift b/Signal/src/Generated/OWSWebRTCDataProtos.pb.swift deleted file mode 100644 index 5ffff701bc..0000000000 --- a/Signal/src/Generated/OWSWebRTCDataProtos.pb.swift +++ /dev/null @@ -1,317 +0,0 @@ -// DO NOT EDIT. -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: OWSWebRTCDataProtos.proto -// -// For information on using the generated types, please see the documenation: -// https://github.com/apple/swift-protobuf/ - -//* -// Copyright (C) 2014-2016 Open Whisper Systems -// -// Licensed according to the LICENSE file in this repository. - -/// iOS - since we use a modern proto-compiler, we must specify -/// the legacy proto format. - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that your are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -struct WebRTCProtos_Connected { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// @required - var id: UInt64 { - get {return _id ?? 0} - set {_id = newValue} - } - /// Returns true if `id` has been explicitly set. - var hasID: Bool {return self._id != nil} - /// Clears the value of `id`. Subsequent reads from it will return its default value. - mutating func clearID() {self._id = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _id: UInt64? = nil -} - -struct WebRTCProtos_Hangup { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// @required - var id: UInt64 { - get {return _id ?? 0} - set {_id = newValue} - } - /// Returns true if `id` has been explicitly set. - var hasID: Bool {return self._id != nil} - /// Clears the value of `id`. Subsequent reads from it will return its default value. - mutating func clearID() {self._id = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _id: UInt64? = nil -} - -struct WebRTCProtos_VideoStreamingStatus { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// @required - var id: UInt64 { - get {return _id ?? 0} - set {_id = newValue} - } - /// Returns true if `id` has been explicitly set. - var hasID: Bool {return self._id != nil} - /// Clears the value of `id`. Subsequent reads from it will return its default value. - mutating func clearID() {self._id = nil} - - var enabled: Bool { - get {return _enabled ?? false} - set {_enabled = newValue} - } - /// Returns true if `enabled` has been explicitly set. - var hasEnabled: Bool {return self._enabled != nil} - /// Clears the value of `enabled`. Subsequent reads from it will return its default value. - mutating func clearEnabled() {self._enabled = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _id: UInt64? = nil - fileprivate var _enabled: Bool? = nil -} - -struct WebRTCProtos_Data { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var connected: WebRTCProtos_Connected { - get {return _storage._connected ?? WebRTCProtos_Connected()} - set {_uniqueStorage()._connected = newValue} - } - /// Returns true if `connected` has been explicitly set. - var hasConnected: Bool {return _storage._connected != nil} - /// Clears the value of `connected`. Subsequent reads from it will return its default value. - mutating func clearConnected() {_storage._connected = nil} - - var hangup: WebRTCProtos_Hangup { - get {return _storage._hangup ?? WebRTCProtos_Hangup()} - set {_uniqueStorage()._hangup = newValue} - } - /// Returns true if `hangup` has been explicitly set. - var hasHangup: Bool {return _storage._hangup != nil} - /// Clears the value of `hangup`. Subsequent reads from it will return its default value. - mutating func clearHangup() {_storage._hangup = nil} - - var videoStreamingStatus: WebRTCProtos_VideoStreamingStatus { - get {return _storage._videoStreamingStatus ?? WebRTCProtos_VideoStreamingStatus()} - set {_uniqueStorage()._videoStreamingStatus = newValue} - } - /// Returns true if `videoStreamingStatus` has been explicitly set. - var hasVideoStreamingStatus: Bool {return _storage._videoStreamingStatus != nil} - /// Clears the value of `videoStreamingStatus`. Subsequent reads from it will return its default value. - mutating func clearVideoStreamingStatus() {_storage._videoStreamingStatus = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _storage = _StorageClass.defaultInstance -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "WebRTCProtos" - -extension WebRTCProtos_Connected: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Connected" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "id"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularUInt64Field(value: &self._id) - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if let v = self._id { - try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - func _protobuf_generated_isEqualTo(other: WebRTCProtos_Connected) -> Bool { - if self._id != other._id {return false} - if unknownFields != other.unknownFields {return false} - return true - } -} - -extension WebRTCProtos_Hangup: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Hangup" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "id"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularUInt64Field(value: &self._id) - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if let v = self._id { - try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - func _protobuf_generated_isEqualTo(other: WebRTCProtos_Hangup) -> Bool { - if self._id != other._id {return false} - if unknownFields != other.unknownFields {return false} - return true - } -} - -extension WebRTCProtos_VideoStreamingStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".VideoStreamingStatus" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "id"), - 2: .same(proto: "enabled"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularUInt64Field(value: &self._id) - case 2: try decoder.decodeSingularBoolField(value: &self._enabled) - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if let v = self._id { - try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) - } - if let v = self._enabled { - try visitor.visitSingularBoolField(value: v, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - func _protobuf_generated_isEqualTo(other: WebRTCProtos_VideoStreamingStatus) -> Bool { - if self._id != other._id {return false} - if self._enabled != other._enabled {return false} - if unknownFields != other.unknownFields {return false} - return true - } -} - -extension WebRTCProtos_Data: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Data" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "connected"), - 2: .same(proto: "hangup"), - 3: .same(proto: "videoStreamingStatus"), - ] - - fileprivate class _StorageClass { - var _connected: WebRTCProtos_Connected? = nil - var _hangup: WebRTCProtos_Hangup? = nil - var _videoStreamingStatus: WebRTCProtos_VideoStreamingStatus? = nil - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _connected = source._connected - _hangup = source._hangup - _videoStreamingStatus = source._videoStreamingStatus - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._connected) - case 2: try decoder.decodeSingularMessageField(value: &_storage._hangup) - case 3: try decoder.decodeSingularMessageField(value: &_storage._videoStreamingStatus) - default: break - } - } - } - } - - func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._connected { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if let v = _storage._hangup { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } - if let v = _storage._videoStreamingStatus { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } - } - try unknownFields.traverse(visitor: &visitor) - } - - func _protobuf_generated_isEqualTo(other: WebRTCProtos_Data) -> Bool { - if _storage !== other._storage { - let storagesAreEqual: Bool = withExtendedLifetime((_storage, other._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let other_storage = _args.1 - if _storage._connected != other_storage._connected {return false} - if _storage._hangup != other_storage._hangup {return false} - if _storage._videoStreamingStatus != other_storage._videoStreamingStatus {return false} - return true - } - if !storagesAreEqual {return false} - } - if unknownFields != other.unknownFields {return false} - return true - } -} diff --git a/Signal/src/Generated/WebRTCProto.swift b/Signal/src/Generated/WebRTCProto.swift deleted file mode 100644 index 3a3d2f0c18..0000000000 --- a/Signal/src/Generated/WebRTCProto.swift +++ /dev/null @@ -1,392 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -import Foundation - -// WARNING: This code is generated. Only edit within the markers. - -public enum WebRTCProtoError: Error { - case invalidProtobuf(description: String) -} - -// MARK: - WebRTCProtoConnected - -@objc public class WebRTCProtoConnected: NSObject { - - // MARK: - WebRTCProtoConnectedBuilder - - @objc public class func builder(id: UInt64) -> WebRTCProtoConnectedBuilder { - return WebRTCProtoConnectedBuilder(id: id) - } - - @objc public class WebRTCProtoConnectedBuilder: NSObject { - - private var proto = WebRTCProtos_Connected() - - @objc fileprivate override init() {} - - @objc fileprivate init(id: UInt64) { - super.init() - - setId(id) - } - - @objc public func setId(_ valueParam: UInt64) { - proto.id = valueParam - } - - @objc public func build() throws -> WebRTCProtoConnected { - return try WebRTCProtoConnected.parseProto(proto) - } - - @objc public func buildSerializedData() throws -> Data { - return try WebRTCProtoConnected.parseProto(proto).serializedData() - } - } - - fileprivate let proto: WebRTCProtos_Connected - - @objc public let id: UInt64 - - private init(proto: WebRTCProtos_Connected, - id: UInt64) { - self.proto = proto - self.id = id - } - - @objc - public func serializedData() throws -> Data { - return try self.proto.serializedData() - } - - @objc public class func parseData(_ serializedData: Data) throws -> WebRTCProtoConnected { - let proto = try WebRTCProtos_Connected(serializedData: serializedData) - return try parseProto(proto) - } - - fileprivate class func parseProto(_ proto: WebRTCProtos_Connected) throws -> WebRTCProtoConnected { - guard proto.hasID else { - throw WebRTCProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") - } - let id = proto.id - - // MARK: - Begin Validation Logic for WebRTCProtoConnected - - - // MARK: - End Validation Logic for WebRTCProtoConnected - - - let result = WebRTCProtoConnected(proto: proto, - id: id) - return result - } -} - -#if DEBUG - -extension WebRTCProtoConnected { - @objc public func serializedDataIgnoringErrors() -> Data? { - return try! self.serializedData() - } -} - -extension WebRTCProtoConnected.WebRTCProtoConnectedBuilder { - @objc public func buildIgnoringErrors() -> WebRTCProtoConnected? { - return try! self.build() - } -} - -#endif - -// MARK: - WebRTCProtoHangup - -@objc public class WebRTCProtoHangup: NSObject { - - // MARK: - WebRTCProtoHangupBuilder - - @objc public class func builder(id: UInt64) -> WebRTCProtoHangupBuilder { - return WebRTCProtoHangupBuilder(id: id) - } - - @objc public class WebRTCProtoHangupBuilder: NSObject { - - private var proto = WebRTCProtos_Hangup() - - @objc fileprivate override init() {} - - @objc fileprivate init(id: UInt64) { - super.init() - - setId(id) - } - - @objc public func setId(_ valueParam: UInt64) { - proto.id = valueParam - } - - @objc public func build() throws -> WebRTCProtoHangup { - return try WebRTCProtoHangup.parseProto(proto) - } - - @objc public func buildSerializedData() throws -> Data { - return try WebRTCProtoHangup.parseProto(proto).serializedData() - } - } - - fileprivate let proto: WebRTCProtos_Hangup - - @objc public let id: UInt64 - - private init(proto: WebRTCProtos_Hangup, - id: UInt64) { - self.proto = proto - self.id = id - } - - @objc - public func serializedData() throws -> Data { - return try self.proto.serializedData() - } - - @objc public class func parseData(_ serializedData: Data) throws -> WebRTCProtoHangup { - let proto = try WebRTCProtos_Hangup(serializedData: serializedData) - return try parseProto(proto) - } - - fileprivate class func parseProto(_ proto: WebRTCProtos_Hangup) throws -> WebRTCProtoHangup { - guard proto.hasID else { - throw WebRTCProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") - } - let id = proto.id - - // MARK: - Begin Validation Logic for WebRTCProtoHangup - - - // MARK: - End Validation Logic for WebRTCProtoHangup - - - let result = WebRTCProtoHangup(proto: proto, - id: id) - return result - } -} - -#if DEBUG - -extension WebRTCProtoHangup { - @objc public func serializedDataIgnoringErrors() -> Data? { - return try! self.serializedData() - } -} - -extension WebRTCProtoHangup.WebRTCProtoHangupBuilder { - @objc public func buildIgnoringErrors() -> WebRTCProtoHangup? { - return try! self.build() - } -} - -#endif - -// MARK: - WebRTCProtoVideoStreamingStatus - -@objc public class WebRTCProtoVideoStreamingStatus: NSObject { - - // MARK: - WebRTCProtoVideoStreamingStatusBuilder - - @objc public class func builder(id: UInt64) -> WebRTCProtoVideoStreamingStatusBuilder { - return WebRTCProtoVideoStreamingStatusBuilder(id: id) - } - - @objc public class WebRTCProtoVideoStreamingStatusBuilder: NSObject { - - private var proto = WebRTCProtos_VideoStreamingStatus() - - @objc fileprivate override init() {} - - @objc fileprivate init(id: UInt64) { - super.init() - - setId(id) - } - - @objc public func setId(_ valueParam: UInt64) { - proto.id = valueParam - } - - @objc public func setEnabled(_ valueParam: Bool) { - proto.enabled = valueParam - } - - @objc public func build() throws -> WebRTCProtoVideoStreamingStatus { - return try WebRTCProtoVideoStreamingStatus.parseProto(proto) - } - - @objc public func buildSerializedData() throws -> Data { - return try WebRTCProtoVideoStreamingStatus.parseProto(proto).serializedData() - } - } - - fileprivate let proto: WebRTCProtos_VideoStreamingStatus - - @objc public let id: UInt64 - - @objc public var enabled: Bool { - return proto.enabled - } - @objc public var hasEnabled: Bool { - return proto.hasEnabled - } - - private init(proto: WebRTCProtos_VideoStreamingStatus, - id: UInt64) { - self.proto = proto - self.id = id - } - - @objc - public func serializedData() throws -> Data { - return try self.proto.serializedData() - } - - @objc public class func parseData(_ serializedData: Data) throws -> WebRTCProtoVideoStreamingStatus { - let proto = try WebRTCProtos_VideoStreamingStatus(serializedData: serializedData) - return try parseProto(proto) - } - - fileprivate class func parseProto(_ proto: WebRTCProtos_VideoStreamingStatus) throws -> WebRTCProtoVideoStreamingStatus { - guard proto.hasID else { - throw WebRTCProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") - } - let id = proto.id - - // MARK: - Begin Validation Logic for WebRTCProtoVideoStreamingStatus - - - // MARK: - End Validation Logic for WebRTCProtoVideoStreamingStatus - - - let result = WebRTCProtoVideoStreamingStatus(proto: proto, - id: id) - return result - } -} - -#if DEBUG - -extension WebRTCProtoVideoStreamingStatus { - @objc public func serializedDataIgnoringErrors() -> Data? { - return try! self.serializedData() - } -} - -extension WebRTCProtoVideoStreamingStatus.WebRTCProtoVideoStreamingStatusBuilder { - @objc public func buildIgnoringErrors() -> WebRTCProtoVideoStreamingStatus? { - return try! self.build() - } -} - -#endif - -// MARK: - WebRTCProtoData - -@objc public class WebRTCProtoData: NSObject { - - // MARK: - WebRTCProtoDataBuilder - - @objc public class func builder() -> WebRTCProtoDataBuilder { - return WebRTCProtoDataBuilder() - } - - @objc public class WebRTCProtoDataBuilder: NSObject { - - private var proto = WebRTCProtos_Data() - - @objc fileprivate override init() {} - - @objc public func setConnected(_ valueParam: WebRTCProtoConnected) { - proto.connected = valueParam.proto - } - - @objc public func setHangup(_ valueParam: WebRTCProtoHangup) { - proto.hangup = valueParam.proto - } - - @objc public func setVideoStreamingStatus(_ valueParam: WebRTCProtoVideoStreamingStatus) { - proto.videoStreamingStatus = valueParam.proto - } - - @objc public func build() throws -> WebRTCProtoData { - return try WebRTCProtoData.parseProto(proto) - } - - @objc public func buildSerializedData() throws -> Data { - return try WebRTCProtoData.parseProto(proto).serializedData() - } - } - - fileprivate let proto: WebRTCProtos_Data - - @objc public let connected: WebRTCProtoConnected? - - @objc public let hangup: WebRTCProtoHangup? - - @objc public let videoStreamingStatus: WebRTCProtoVideoStreamingStatus? - - private init(proto: WebRTCProtos_Data, - connected: WebRTCProtoConnected?, - hangup: WebRTCProtoHangup?, - videoStreamingStatus: WebRTCProtoVideoStreamingStatus?) { - self.proto = proto - self.connected = connected - self.hangup = hangup - self.videoStreamingStatus = videoStreamingStatus - } - - @objc - public func serializedData() throws -> Data { - return try self.proto.serializedData() - } - - @objc public class func parseData(_ serializedData: Data) throws -> WebRTCProtoData { - let proto = try WebRTCProtos_Data(serializedData: serializedData) - return try parseProto(proto) - } - - fileprivate class func parseProto(_ proto: WebRTCProtos_Data) throws -> WebRTCProtoData { - var connected: WebRTCProtoConnected? - if proto.hasConnected { - connected = try WebRTCProtoConnected.parseProto(proto.connected) - } - - var hangup: WebRTCProtoHangup? - if proto.hasHangup { - hangup = try WebRTCProtoHangup.parseProto(proto.hangup) - } - - var videoStreamingStatus: WebRTCProtoVideoStreamingStatus? - if proto.hasVideoStreamingStatus { - videoStreamingStatus = try WebRTCProtoVideoStreamingStatus.parseProto(proto.videoStreamingStatus) - } - - // MARK: - Begin Validation Logic for WebRTCProtoData - - - // MARK: - End Validation Logic for WebRTCProtoData - - - let result = WebRTCProtoData(proto: proto, - connected: connected, - hangup: hangup, - videoStreamingStatus: videoStreamingStatus) - return result - } -} - -#if DEBUG - -extension WebRTCProtoData { - @objc public func serializedDataIgnoringErrors() -> Data? { - return try! self.serializedData() - } -} - -extension WebRTCProtoData.WebRTCProtoDataBuilder { - @objc public func buildIgnoringErrors() -> WebRTCProtoData? { - return try! self.build() - } -} - -#endif diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 43ddc064b3..42a791de13 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -4,71 +4,77 @@ import Foundation import PromiseKit +import SignalRingRTC import WebRTC import SignalServiceKit import SignalMessaging /** - * `CallService` is a global singleton that manages the state of WebRTC-backed Signal Calls - * (as opposed to legacy "RedPhone Calls"). + * `CallService` is a global singleton that manages the state of RingRTC-backed Signal + * Calls (as opposed to legacy "RedPhone Calls" and "WebRTC Calls"). * * It serves as a connection between the `CallUIAdapter` and the `PeerConnectionClient`. + * The PeerConnectionClient itself is a thin wrapper around the RingRTC `CallConnection`. * * ## Signaling * - * Signaling refers to the setup and tear down of the connection. Before the connection is established, this must happen - * out of band (using Signal Service), but once the connection is established it's possible to publish updates - * (like hangup) via the established channel. + * Signaling refers to the setup and tear down of the connection. Before the connection + * is established, this must happen out of band (using the Signal Service), but once the + * connection is established it's possible to publish updates (like hangup) via the + * established channel. * - * Signaling state is synchronized on the main thread and only mutated in the handleXXX family of methods. + * Signaling state is synchronized on the main thread and only mutated in the handleXXX + * family of methods. * - * Following is a high level process of the exchange of messages that takes place during call signaling. + * Following is a high level process of the exchange of messages that takes place during + * call signaling. * * ### Key * - * --[SOMETHING]--> represents a message of type "Something" sent from the caller to the callee - * <--[SOMETHING]-- represents a message of type "Something" sent from the callee to the caller + * --[SOMETHING]--> represents a message from the caller to the callee + * <--[SOMETHING]-- represents a message from the callee to the caller * SS: Message sent via Signal Service - * DC: Message sent via WebRTC Data Channel + * DC: Message sent via RingRTC (using a WebRTC Data Channel) * * ### Message Exchange / State Flow Overview * - * | Caller | Callee | - * +----------------------------+-------------------------+ + * | Caller | Callee | + * +------------------------+----------------------------------------+ * Start outgoing call: `handleOutgoingCall`... - --[SS.CallOffer]--> + * --[SS.CallOffer]--> * ...and start generating ICE updates. - * As ICE candidates are generated, `handleLocalAddedIceCandidate` is called. - * and we *store* the ICE updates for later. * - * Received call offer: `handleReceivedOffer` - * Send call answer - * <--[SS.CallAnswer]-- - * Start generating ICE updates. - * As they are generated `handleLocalAddedIceCandidate` is called - which immediately sends the ICE updates to the Caller. - * <--[SS.ICEUpdate]-- (sent multiple times) + * As ICE candidates are generated, `handleLocalAddedIceCandidate` + * is called and we *store* the ICE updates for later. + * + * Received call offer: `handleReceivedOffer` + * Send call answer + * <--[SS.CallAnswer]-- + * ... and start generating ICE updates. + * + * As ICE candidates are generated, `handleLocalAddedIceCandidate` + * is called which immediately sends the ICE updates to the Caller. + * <--[SS.ICEUpdate]-- (sent multiple times) * * Received CallAnswer: `handleReceivedAnswer` * So send any stored ice updates (and send future ones immediately) - * --[SS.ICEUpdates]--> + * --[SS.ICEUpdates]--> * - * Once compatible ICE updates have been exchanged... - * both parties: `handleIceConnected` + * Once compatible ICE updates have been exchanged both parties: `handleRinging`: * - * Show remote ringing UI - * Connect to offered Data Channel - * Show incoming call UI. + * Show remote ringing UI... + * Show incoming call UI... * - * If callee answers Call - * send connected message - * <--[DC.ConnectedMesage]-- - * Received connected message + * If callee answers Call send `connected` message by invoking + * the `acceptCall` PeerConnectionClient API. + * + * <--[DC.ConnectedMesage]-- + * Received connected message via remoteConnected call event... * Show Call is connected. * * Hang up (this could equally be sent by the Callee) - * --[DC.Hangup]--> - * --[SS.Hangup]--> + * --[DC.Hangup]--> (via PeerConnectionClient API) + * --[SS.Hangup]--> */ public enum CallError: Error { @@ -82,9 +88,6 @@ public enum CallError: Error { case messageSendFailure(underlyingError: Error) } -// Should be roughly synced with Android client for consistency -private let connectingTimeoutSeconds: TimeInterval = 120 - // All Observer methods will be invoked from the main thread. protocol CallServiceObserver: class { /** @@ -111,9 +114,7 @@ private class SignalCallData: NSObject { public let call: SignalCall - // Used to coordinate promises across delegate methods - let callConnectedPromise: Promise - let callConnectedResolver: Resolver + public var backgroundTask: OWSBackgroundTask? // Used to ensure any received ICE messages wait until the peer connection client is set up. let peerConnectionClientPromise: Promise @@ -159,10 +160,6 @@ private class SignalCallData: NSObject { self.call = call self.delegate = delegate - let (callConnectedPromise, callConnectedResolver) = Promise.pending() - self.callConnectedPromise = callConnectedPromise - self.callConnectedResolver = callConnectedResolver - let (peerConnectionClientPromise, peerConnectionClientResolver) = Promise.pending() self.peerConnectionClientPromise = peerConnectionClientPromise self.peerConnectionClientResolver = peerConnectionClientResolver @@ -187,10 +184,6 @@ private class SignalCallData: NSObject { self.call.removeAllObservers() - // In case we're still waiting on this promise somewhere, we need to reject it to avoid a memory leak. - // There is no harm in rejecting a previously fulfilled promise. - self.callConnectedResolver.reject(CallError.obsoleteCall(description: "Terminating call")) - // In case we're still waiting on the peer connection setup somewhere, we need to reject it to avoid a memory leak. // There is no harm in rejecting a previously fulfilled promise. self.peerConnectionClientResolver.reject(CallError.obsoleteCall(description: "Terminating call")) @@ -199,8 +192,7 @@ private class SignalCallData: NSObject { // There is no harm in rejecting a previously fulfilled promise. self.readyToSendIceUpdatesResolver.reject(CallError.obsoleteCall(description: "Terminating call")) - peerConnectionClient?.terminate() - Logger.debug("setting peerConnectionClient") + peerConnectionClient?.close() outgoingIceUpdateQueue.removeAll() } @@ -334,6 +326,7 @@ private class SignalCallData: NSObject { return callData?.call } } + var peerConnectionClient: PeerConnectionClient? { get { AssertIsOnMainThread() @@ -451,7 +444,6 @@ private class SignalCallData: NSObject { let errorDescription = "call was unexpectedly already set." Logger.error(errorDescription) call.state = .localFailure - OWSProdError(OWSAnalyticsEvents.callServiceCallAlreadySet(), file: #file, function: #function, line: #line) return Promise(error: CallError.assertionError(description: errorDescription)) } @@ -465,8 +457,7 @@ private class SignalCallData: NSObject { } call.callRecord = callRecord - let promise = getIceServers() - .then { iceServers -> Promise in + let promise = getIceServers().done { iceServers in Logger.debug("got ice servers:\(iceServers) for call: \(call.identifiersForLogs)") guard self.call == call else { @@ -476,80 +467,25 @@ private class SignalCallData: NSObject { guard callData.peerConnectionClient == nil else { let errorDescription = "peerConnectionClient was unexpectedly already set." Logger.error(errorDescription) - OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionAlreadySet(), file: #file, function: #function, line: #line) throw CallError.assertionError(description: errorDescription) } let useTurnOnly = Environment.shared.preferences.doCallsHideIPAddress() - let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self, callDirection: .outgoing, useTurnOnly: useTurnOnly) + // Create a PeerConnectionClient to handle the call media. + let peerConnectionClient = PeerConnectionClient(delegate: self) Logger.debug("setting peerConnectionClient for call: \(call.identifiersForLogs)") + callData.peerConnectionClient = peerConnectionClient callData.peerConnectionClientResolver.fulfill(()) - return peerConnectionClient.createOffer() - }.then { (sessionDescription: HardenedRTCSessionDescription) -> Promise in - guard self.call == call else { - throw CallError.obsoleteCall(description: "obsolete call") - } - guard let peerConnectionClient = self.peerConnectionClient else { - owsFailDebug("Missing peerConnectionClient") - throw CallError.obsoleteCall(description: "Missing peerConnectionClient") - } - - Logger.info("session description for outgoing call: \(call.identifiersForLogs), sdp: \(sessionDescription.logSafeDescription).") - - return - peerConnectionClient.setLocalSessionDescription(sessionDescription) - .then { _ -> Promise in - do { - let offerBuilder = SSKProtoCallMessageOffer.builder(id: call.signalingId, - sessionDescription: sessionDescription.sdp) - let callMessage = OWSOutgoingCallMessage(thread: call.thread, offerMessage: try offerBuilder.build()) - return self.messageSender.sendMessage(.promise, callMessage.asPreparer) - } catch { - owsFailDebug("Couldn't build proto") - throw CallError.fatalError(description: "Couldn't build proto") - } - } - }.then { () -> Promise in - guard self.call == call else { - throw CallError.obsoleteCall(description: "obsolete call") - } - - // For outgoing calls, wait until call offer is sent before we send any ICE updates, to ensure message ordering for - // clients that don't support receiving ICE updates before receiving the call offer. - self.readyToSendIceUpdates(call: call) - - // Don't let the outgoing call ring forever. We don't support inbound ringing forever anyway. - let timeout: Promise = after(seconds: connectingTimeoutSeconds).done { - // This code will always be called, whether or not the call has timed out. - // However, if the call has already connected, the `race` promise will have already been - // fulfilled. Rejecting an already fulfilled promise is a no-op. - throw CallError.timeout(description: "timed out waiting to receive call answer") - } - - return race(timeout, callData.callConnectedPromise) - }.done { - Logger.info(self.call == call - ? "outgoing call connected: \(call.identifiersForLogs)." - : "obsolete outgoing call connected: \(call.identifiersForLogs).") + try peerConnectionClient.sendOffer(iceServers: iceServers, useTurnOnly: useTurnOnly, callId: callId) } promise.catch { error in - Logger.error("placing call \(call.identifiersForLogs) failed with error: \(error)") + Logger.error("outgoing call \(call.identifiersForLogs) failed with error: \(error)") - if let callError = error as? CallError { - if case .timeout = callError { - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorTimeoutWhileConnectingOutgoing(), file: #file, function: #function, line: #line) - } - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorOutgoingConnectionFailedInternal(), file: #file, function: #function, line: #line) - self.handleFailedCall(failedCall: call, error: callError) - } else { - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorOutgoingConnectionFailedExternal(), file: #file, function: #function, line: #line) - let externalError = CallError.externalError(underlyingError: error) - self.handleFailedCall(failedCall: call, error: externalError) - } + self.handleFailedCall(failedCall: call, error: error) }.retainUntilComplete() return promise @@ -559,7 +495,7 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() guard let callData = self.callData else { - self.handleFailedCall(failedCall: call, error: .obsoleteCall(description:"obsolete call")) + self.handleFailedCall(failedCall: call, error: CallError.obsoleteCall(description: "obsolete call")) return } guard callData.call == call else { @@ -588,26 +524,17 @@ private class SignalCallData: NSObject { } guard let peerConnectionClient = self.peerConnectionClient else { - OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionMissing(), file: #file, function: #function, line: #line) handleFailedCall(failedCall: call, error: CallError.assertionError(description: "peerConnectionClient was unexpectedly nil")) return } - let sessionDescription = RTCSessionDescription(type: .answer, sdp: sessionDescription) + do { + try peerConnectionClient.receivedAnswer(sdp: sessionDescription) + } catch { + Logger.debug("receivedAnswer failed with \(error)") - peerConnectionClient.setRemoteSessionDescription(sessionDescription) - .done { - Logger.debug("successfully set remote description") - }.catch { error in - if let callError = error as? CallError { - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleReceivedErrorInternal(), file: #file, function: #function, line: #line) - self.handleFailedCall(failedCall: call, error: callError) - } else { - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleReceivedErrorExternal(), file: #file, function: #function, line: #line) - let externalError = CallError.externalError(underlyingError: error) - self.handleFailedCall(failedCall: call, error: externalError) - } - }.retainUntilComplete() + self.handleFailedCall(failedCall: call, error: error) + } } /** @@ -624,7 +551,7 @@ private class SignalCallData: NSObject { } guard let callRecord = call.callRecord else { - handleFailedCall(failedCall: call, error: .assertionError(description: "callRecord was unexpectedly nil")) + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "callRecord was unexpectedly nil")) return } @@ -784,7 +711,8 @@ private class SignalCallData: NSObject { let callData = SignalCallData(call: newCall, delegate: self) self.callData = callData - var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in + Logger.debug("Enable backgroundTask") + let backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in AssertIsOnMainThread() guard status == .expired else { @@ -794,6 +722,7 @@ private class SignalCallData: NSObject { guard let strongSelf = self else { return } + let timeout = CallError.timeout(description: "background task time ran out before call connected.") guard strongSelf.call == newCall else { @@ -803,13 +732,17 @@ private class SignalCallData: NSObject { strongSelf.handleFailedCall(failedCall: newCall, error: timeout) }) - getIceServers() - .then { (iceServers: [RTCIceServer]) -> Promise in + callData.backgroundTask = backgroundTask + + getIceServers().done { iceServers 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. + Logger.debug("got ice servers:\(iceServers) for call: \(newCall.identifiersForLogs)") + guard self.call == newCall else { throw CallError.obsoleteCall(description: "getIceServers() response for obsolete call") } + assert(self.peerConnectionClient == nil, "Unexpected PeerConnectionClient instance") // For contacts not stored in our system contacts, we assume they are an unknown caller, and we force @@ -818,73 +751,23 @@ private class SignalCallData: NSObject { let useTurnOnly = isUnknownCaller || Environment.shared.preferences.doCallsHideIPAddress() - Logger.debug("setting peerConnectionClient for: \(newCall.identifiersForLogs)") - let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self, callDirection: .incoming, useTurnOnly: useTurnOnly) + // Create a PeerConnectionClient to handle the call media. + let peerConnectionClient = PeerConnectionClient(delegate: self) + Logger.debug("setting peerConnectionClient for call: \(newCall.identifiersForLogs)") + callData.peerConnectionClient = peerConnectionClient callData.peerConnectionClientResolver.fulfill(()) - let offerSessionDescription = RTCSessionDescription(type: .offer, sdp: callerSessionDescription) - let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) - - // Find a sessionDescription compatible with my constraints and the remote sessionDescription - return peerConnectionClient.negotiateSessionDescription(remoteDescription: offerSessionDescription, constraints: constraints) - }.then { (negotiatedSessionDescription: HardenedRTCSessionDescription) -> Promise in - guard self.call == newCall else { - throw CallError.obsoleteCall(description: "negotiateSessionDescription() response for obsolete call") - } - - Logger.info("session description for incoming call: \(newCall.identifiersForLogs), sdp: \(negotiatedSessionDescription.logSafeDescription).") - - do { - let answerBuilder = SSKProtoCallMessageAnswer.builder(id: newCall.signalingId, - sessionDescription: negotiatedSessionDescription.sdp) - let callAnswerMessage = OWSOutgoingCallMessage(thread: thread, answerMessage: try answerBuilder.build()) - - return self.messageSender.sendMessage(.promise, callAnswerMessage.asPreparer) - } catch { - owsFailDebug("Couldn't build proto") - throw CallError.fatalError(description: "Couldn't build proto") - } - }.then { () -> Promise in - guard self.call == newCall else { - throw CallError.obsoleteCall(description: "sendMessage response for obsolete call") - } - Logger.debug("successfully sent callAnswerMessage for: \(newCall.identifiersForLogs)") - - // There's nothing technically forbidding receiving ICE updates before receiving the CallAnswer, but this - // a more intuitive ordering. - self.readyToSendIceUpdates(call: newCall) - - let timeout: Promise = after(seconds: connectingTimeoutSeconds).done { - // rejecting a promise by throwing is safely a no-op if the promise has already been fulfilled - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorTimeoutWhileConnectingIncoming(), file: #file, function: #function, line: #line) - throw CallError.timeout(description: "timed out waiting for call to connect") - } - - // This will be fulfilled (potentially) by the RTCDataChannel delegate method - return race(callData.callConnectedPromise, timeout) - }.done { - Logger.info(self.call == newCall - ? "incoming call connected: \(newCall.identifiersForLogs)." - : "obsolete incoming call connected: \(newCall.identifiersForLogs).") + try peerConnectionClient.receivedOffer(iceServers: iceServers, useTurnOnly: useTurnOnly, callId: callId, sdp: callerSessionDescription) }.recover { error in + Logger.error("incoming call \(newCall.identifiersForLogs) failed with error: \(error)") + guard self.call == newCall else { - Logger.debug("ignoring error: \(error) for obsolete call: \(newCall.identifiersForLogs).") + Logger.debug("ignoring error for obsolete call") return } - if let callError = error as? CallError { - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorIncomingConnectionFailedInternal(), file: #file, function: #function, line: #line) - self.handleFailedCall(failedCall: newCall, error: callError) - } else { - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorIncomingConnectionFailedExternal(), file: #file, function: #function, line: #line) - let externalError = CallError.externalError(underlyingError: error) - self.handleFailedCall(failedCall: newCall, error: externalError) - } - }.ensure { - Logger.debug("ending background task awaiting inbound call connection") - assert(backgroundTask != nil) - backgroundTask = nil + self.handleFailedCall(failedCall: newCall, error: error) }.retainUntilComplete() } @@ -893,7 +776,7 @@ private class SignalCallData: NSObject { */ public func handleRemoteAddedIceCandidate(thread: TSContactThread, callId: UInt64, sdp: String, lineIndex: Int32, mid: String) { AssertIsOnMainThread() - Logger.verbose("callId: \(callId)") + Logger.verbose("forming callId: \(callId)") guard let callData = self.callData else { Logger.info("ignoring remote ice update, since there is no current call.") @@ -903,6 +786,8 @@ private class SignalCallData: NSObject { callData.peerConnectionClientPromise.done { AssertIsOnMainThread() + Logger.verbose("running callId: \(callId)") + guard let call = self.call else { Logger.warn("ignoring remote ice update for thread: \(String(describing: thread.uniqueId)) since there is no current call. Call already ended?") return @@ -923,11 +808,11 @@ private class SignalCallData: NSObject { return } - Logger.verbose("addRemoteIceCandidate") - peerConnectionClient.addRemoteIceCandidate(RTCIceCandidate(sdp: sdp, sdpMLineIndex: lineIndex, sdpMid: mid)) - }.catch { error in - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleRemoteAddedIceCandidate(), file: #file, function: #function, line: #line) - Logger.error("peerConnectionClientPromise failed with error: \(error)") + try peerConnectionClient.receivedIceCandidate(sdp: sdp, lineIndex: lineIndex, sdpMid: mid) + }.catch { error in + Logger.error("handleRemoteAddedIceCandidate failed with error: \(error)") + + // @note We will move on and try to connect the call. }.retainUntilComplete() } @@ -935,14 +820,9 @@ private class SignalCallData: NSObject { * Local client (could be caller or callee) generated some connectivity information that we should send to the * remote client. */ - private func handleLocalAddedIceCandidate(_ iceCandidate: RTCIceCandidate) { + private func handleLocalAddedIceCandidate(_ iceCandidate: RTCIceCandidate, callData: SignalCallData) { AssertIsOnMainThread() - guard let callData = self.callData else { - OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) - self.handleFailedCurrentCall(error: CallError.assertionError(description: "ignoring local ice candidate, since there is no current call.")) - return - } let call = callData.call // Wait until we've sent the CallOffer before sending any ice updates for the call to ensure @@ -956,7 +836,6 @@ private class SignalCallData: NSObject { guard call.state != .idle else { // This will only be called for the current peerConnectionClient, so // fail the current call. - OWSProdError(OWSAnalyticsEvents.callServiceCallUnexpectedlyIdle(), file: #file, function: #function, line: #line) self.handleFailedCurrentCall(error: CallError.assertionError(description: "ignoring local ice candidate, since call is now idle.")) return } @@ -998,18 +877,17 @@ private class SignalCallData: NSObject { } /** - * The clients can now communicate via WebRTC. + * The clients can now communicate via WebRTC, so we can let the UI know. * * Called by both caller and callee. Compatible ICE messages have been exchanged between the local and remote * client. */ - private func handleIceConnected() { + private func handleRinging() { AssertIsOnMainThread() guard let callData = self.callData else { // This will only be called for the current peerConnectionClient, so // fail the current call. - OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "ignoring \(#function) since there is no current call.")) return } @@ -1041,13 +919,12 @@ private class SignalCallData: NSObject { } } - private func handleIceDisconnected() { + private func handleReconnecting() { AssertIsOnMainThread() guard let call = self.call else { // This will only be called for the current peerConnectionClient, so // fail the current call. - OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "ignoring \(#function) since there is no current call.")) return } @@ -1099,6 +976,7 @@ private class SignalCallData: NSObject { } call.state = .remoteHangup + // Notify UI callUIAdapter.remoteDidHangupCall(call) @@ -1117,7 +995,6 @@ private class SignalCallData: NSObject { guard let call = self.call else { // This should never happen; return to a known good state. owsFailDebug("call was unexpectedly nil") - OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "call was unexpectedly nil")) return } @@ -1125,7 +1002,6 @@ private class SignalCallData: NSObject { guard call.localId == localId else { // This should never happen; return to a known good state. owsFailDebug("callLocalId:\(localId) doesn't match current calls: \(call.localId)") - OWSProdError(OWSAnalyticsEvents.callServiceCallIdMismatch(), file: #file, function: #function, line: #line) handleFailedCurrentCall(error: CallError.assertionError(description: "callLocalId:\(localId) doesn't match current calls: \(call.localId)")) return } @@ -1142,7 +1018,6 @@ private class SignalCallData: NSObject { Logger.debug("") guard let currentCallData = self.callData else { - OWSProdError(OWSAnalyticsEvents.callServiceCallDataMissing(), file: #file, function: #function, line: #line) handleFailedCall(failedCall: call, error: CallError.assertionError(description: "callData unexpectedly nil")) return } @@ -1155,7 +1030,6 @@ private class SignalCallData: NSObject { } guard let peerConnectionClient = self.peerConnectionClient else { - OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionMissing(), file: #file, function: #function, line: #line) handleFailedCall(failedCall: call, error: CallError.assertionError(description: "missing peerConnection client")) return } @@ -1169,19 +1043,13 @@ private class SignalCallData: NSObject { } call.callRecord = callRecord - var messageData: Data do { - let connectedBuilder = WebRTCProtoConnected.builder(id: call.signalingId) - let dataBuilder = WebRTCProtoData.builder() - dataBuilder.setConnected(try connectedBuilder.build()) - messageData = try dataBuilder.buildSerializedData() + try peerConnectionClient.acceptCall() } catch { - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build proto")) + self.handleFailedCall(failedCall: call, error: error) return } - peerConnectionClient.sendDataChannelMessage(data: messageData, description: "connected", isCritical: true) - handleConnectedCall(currentCallData) } @@ -1194,21 +1062,25 @@ private class SignalCallData: NSObject { Logger.info("") guard let peerConnectionClient = callData.peerConnectionClient else { - OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionMissing(), file: #file, function: #function, line: #line) handleFailedCall(failedCall: call, error: CallError.assertionError(description: "peerConnectionClient unexpectedly nil")) return } Logger.info("handleConnectedCall: \(callData.call.identifiersForLogs).") - // cancel connection timeout - callData.callConnectedResolver.fulfill(()) + // End the background task. + callData.backgroundTask = nil callData.call.state = .connected // We don't risk transmitting any media until the remote client has admitted to being connected. ensureAudioState(call: callData.call, peerConnectionClient: peerConnectionClient) - peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) + + do { + try peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) + } catch { + Logger.error("peerConnectionClient.setLocalVideoEnabled failed \(error)") + } } /** @@ -1228,7 +1100,6 @@ private class SignalCallData: NSObject { } guard call == currentCall else { - OWSProdError(OWSAnalyticsEvents.callServiceCallMismatch(), file: #file, function: #function, line: #line) handleFailedCall(failedCall: call, error: CallError.assertionError(description: "ignoring \(#function) for call other than current call")) return } @@ -1254,48 +1125,41 @@ private class SignalCallData: NSObject { call.state = .localHangup - // TODO something like this lifted from Signal-Android. - // this.accountManager.cancelInFlightRequests(); - // this.messageSender.cancelInFlightRequests(); - if let peerConnectionClient = self.peerConnectionClient { // Stop audio capture ASAP ensureAudioState(call: call, peerConnectionClient: peerConnectionClient) - // If the call is connected, we can send the hangup via the data channel for faster hangup. - - var messageData: Data + // Hangup a call, should send 'hangup' message via data channel. Will + // also call onSendHangup which will send a 'hangup' message via + // signaling and actually terminate the call. do { - let hangupBuilder = WebRTCProtoHangup.builder(id: call.signalingId) - let dataBuilder = WebRTCProtoData.builder() - dataBuilder.setHangup(try hangupBuilder.build()) - messageData = try dataBuilder.buildSerializedData() + try peerConnectionClient.hangup() } catch { - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build proto")) + self.handleFailedCall(failedCall: call, error: error) return } - - peerConnectionClient.sendDataChannelMessage(data: messageData, description: "hangup", isCritical: true) } else { Logger.info("ending call before peer connection created. Device offline or quick hangup.") - } - // If the call hasn't started yet, we don't have a data channel to communicate the hang up. Use Signal Service Message. - do { - let hangupBuilder = SSKProtoCallMessageHangup.builder(id: call.signalingId) - let callMessage = OWSOutgoingCallMessage(thread: call.thread, hangupMessage: try hangupBuilder.build()) + // If the call hasn't started yet, we don't have a data channel to communicate the hang up. Use Signal Service Message. + do { + let hangupBuilder = SSKProtoCallMessageHangup.builder(id: call.signalingId) + let callMessage = OWSOutgoingCallMessage(thread: call.thread, hangupMessage: try hangupBuilder.build()) + let sendPromise = messageSender.sendMessage(.promise, callMessage.asPreparer) + .done { + Logger.debug("sent hangup call message to \(call.thread.contactAddress)") - self.messageSender.sendMessage(.promise, callMessage.asPreparer) - .done { - Logger.debug("successfully sent hangup call message to \(call.thread.contactAddress)") - }.catch { error in - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleLocalHungupCall(), file: #file, function: #function, line: #line) - Logger.error("failed to send hangup call message to \(call.thread.contactAddress) with error: \(error)") - }.retainUntilComplete() + self.terminateCall() + }.catch { error in + OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleLocalHungupCall(), file: #file, function: #function, line: #line) + Logger.error("failed to send hangup call message to \(call.thread.contactAddress) with error: \(error)") + self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "failed to send hangup call message")) + } - terminateCall() - } catch { - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build proto")) + sendPromise.retainUntilComplete() + } catch { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build hangup proto")) + } } } @@ -1347,19 +1211,35 @@ private class SignalCallData: NSObject { func ensureAudioState(call: SignalCall, peerConnectionClient: PeerConnectionClient) { guard call.state == .connected else { - peerConnectionClient.setAudioEnabled(enabled: false) + do { + try peerConnectionClient.setLocalAudioEnabled(enabled: false) + } catch { + Logger.error("peerConnectionClient.setLocalAudioEnabled failed") + } return } guard !call.isMuted else { - peerConnectionClient.setAudioEnabled(enabled: false) + do { + try peerConnectionClient.setLocalAudioEnabled(enabled: false) + } catch { + Logger.error("peerConnectionClient.setLocalAudioEnabled failed") + } return } guard !call.isOnHold else { - peerConnectionClient.setAudioEnabled(enabled: false) + do { + try peerConnectionClient.setLocalAudioEnabled(enabled: false) + } catch { + Logger.error("peerConnectionClient.setLocalAudioEnabled failed") + } return } - peerConnectionClient.setAudioEnabled(enabled: true) + do { + try peerConnectionClient.setLocalAudioEnabled(enabled: true) + } catch { + Logger.error("peerConnectionClient.setLocalAudioEnabled failed") + } } /** @@ -1380,7 +1260,7 @@ private class SignalCallData: NSObject { return } - if (granted) { + if granted { // Success callback; camera permissions are granted. strongSelf.setHasLocalVideoWithCameraPermissions(hasLocalVideo: hasLocalVideo) } else { @@ -1413,7 +1293,11 @@ private class SignalCallData: NSObject { } if call.state == .connected { - peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) + do { + try peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) + } catch { + Logger.error("peerConnectionClient.setLocalVideoEnabled failed \(error)") + } } } @@ -1431,213 +1315,346 @@ private class SignalCallData: NSObject { return } - peerConnectionClient.setCameraSource(isUsingFrontCamera: isUsingFrontCamera) - } - - /** - * Local client received a message on the WebRTC data channel. - * - * The WebRTC data channel is a faster signaling channel than out of band Signal Service messages. Once it's - * established we use it to communicate further signaling information. The one sort-of exception is that with - * hangup messages we redundantly send a Signal Service hangup message, which is more reliable, and since the hangup - * action is idemptotent, there's no harm done. - * - * Used by both Incoming and Outgoing calls. - */ - private func handleDataChannelMessage(_ message: WebRTCProtoData) { - AssertIsOnMainThread() - - guard let callData = self.callData else { - // This should never happen; return to a known good state. - owsFailDebug("received data message, but there is no current call. Ignoring.") - OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) - handleFailedCurrentCall(error: CallError.assertionError(description: "received data message, but there is no current call. Ignoring.")) - return + do { + try peerConnectionClient.setCameraSource(isUsingFrontCamera: isUsingFrontCamera) + } catch { + Logger.error("peerConnectionClient.setCameraSource failed") } - let call = callData.call - - if let connected = message.connected { - handleIncomingConnected(call: call, - callData: callData, - connected: connected) - } else if let hangup = message.hangup { - handleIncomingHangup(call: call, - callData: callData, - hangup: hangup) - } else if let videoStreamingStatus = message.videoStreamingStatus { - handleIncomingVideoStreamingStatus(call: call, - callData: callData, - videoStreamingStatus: videoStreamingStatus) - } else { - Logger.info("received unknown or empty DataChannelMessage: \(call.identifiersForLogs).") - } - } - - private func handleIncomingConnected(call: SignalCall, - callData: SignalCallData, - connected: WebRTCProtoConnected) { - AssertIsOnMainThread() - - Logger.debug("remote participant sent Connected via data channel: \(call.identifiersForLogs).") - - guard call.direction == .outgoing else { - owsFailDebug("Received connected message.") - handleFailedCurrentCall(error: CallError.assertionError(description: "Received connected message.")) - return - } - guard call.state == .remoteRinging else { - owsFailDebug("Not remote ringing.") - return - } - - guard connected.id == call.signalingId else { - // This should never happen; return to a known good state. - owsFailDebug("received connected message for call with id:\(connected.id) but current call has id:\(call.signalingId)") - OWSProdError(OWSAnalyticsEvents.callServiceCallIdMismatch(), file: #file, function: #function, line: #line) - handleFailedCurrentCall(error: CallError.assertionError(description: "received connected message for call with id:\(connected.id) but current call has id:\(call.signalingId)")) - return - } - - self.callUIAdapter.recipientAcceptedCall(call) - handleConnectedCall(callData) - } - - private func handleIncomingHangup(call: SignalCall, - callData: SignalCallData, - hangup: WebRTCProtoHangup) { - AssertIsOnMainThread() - - Logger.debug("remote participant sent Hangup via data channel: \(call.identifiersForLogs).") - - guard hangup.id == call.signalingId else { - // This should never happen; return to a known good state. - owsFailDebug("received hangup message for call with id:\(hangup.id) but current call has id:\(call.signalingId)") - OWSProdError(OWSAnalyticsEvents.callServiceCallIdMismatch(), file: #file, function: #function, line: #line) - handleFailedCurrentCall(error: CallError.assertionError(description: "received hangup message for call with id:\(hangup.id) but current call has id:\(call.signalingId)")) - return - } - - handleRemoteHangup(thread: call.thread, callId: hangup.id) - } - - private func handleIncomingVideoStreamingStatus(call: SignalCall, - callData: SignalCallData, - videoStreamingStatus: WebRTCProtoVideoStreamingStatus) { - Logger.debug("remote participant sent VideoStreamingStatus via data channel: \(call.identifiersForLogs).") - - guard callData.isRemoteVideoEnabled != videoStreamingStatus.enabled else { - Logger.warn("Redundant video streaming status.") - return - } - - callData.isRemoteVideoEnabled = videoStreamingStatus.enabled - self.fireDidUpdateVideoTracks() } // MARK: - PeerConnectionClientDelegate - /** - * The connection has been established. The clients can now communicate. - */ - internal func peerConnectionClientIceConnected(_ peerConnectionClient: PeerConnectionClient) { + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onCallEvent event: CallEvent, callId: UInt64) { AssertIsOnMainThread() guard peerConnectionClient == self.peerConnectionClient else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") + Logger.debug("Ignoring event from obsolete client") return } - self.handleIceConnected() + guard let callData = self.callData else { + Logger.debug("Ignoring event from obsolete call") + return + } + + let call = callData.call + + guard callId == call.signalingId else { + owsFailDebug("received call event for call with id:\(callId) but current call has id:\(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "received call event for call with id:\(callId) but current call has id:\(call.signalingId)")) + return + } + + switch event { + case .ringing: + Logger.debug("Got ringing.") + + // The underlying connection is established, notify the user. + handleRinging() + + case .remoteConnected: + Logger.debug("Got remoteConnected, the peer (callee) accepted the call: \(call.identifiersForLogs)") + + callUIAdapter.recipientAcceptedCall(call) + handleConnectedCall(callData) + + case .remoteVideoEnable: + Logger.debug("remote participant sent VideoStreamingStatus via data channel: \(call.identifiersForLogs).") + + callData.isRemoteVideoEnabled = true + fireDidUpdateVideoTracks() + + case .remoteVideoDisable: + Logger.debug("remote participant sent VideoStreamingStatus via data channel: \(call.identifiersForLogs).") + + callData.isRemoteVideoEnabled = false + fireDidUpdateVideoTracks() + + case .remoteHangup: + Logger.debug("Got remoteHangup: \(call.identifiersForLogs)") + + handleRemoteHangup(thread: call.thread, callId: callId) + + case .connectionFailed: + Logger.debug("Got connectionFailed.") + + // Return to a known good state. + self.handleFailedCurrentCall(error: CallError.disconnected) + + case .callTimeout: + Logger.debug("Got callTimeout.") + + let description: String + + if call.direction == .outgoing { + description = "timeout for outgoing call" + } else { + description = "timeout for incoming call" + } + + handleFailedCall(failedCall: call, error: CallError.timeout(description: description)) + + case .callReconnecting: + Logger.debug("Got callReconnecting.") + + self.handleReconnecting() + } } - func peerConnectionClientIceDisconnected(_ peerConnectionClient: PeerConnectionClient) { + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onCallError error: String, callId: UInt64) { AssertIsOnMainThread() + Logger.debug("Got an error from RingRTC: \(error)") + guard peerConnectionClient == self.peerConnectionClient else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") + Logger.debug("Ignoring event from obsolete client") return } - self.handleIceDisconnected() + guard let callData = self.callData else { + Logger.debug("Ignoring event from obsolete call") + return + } + + let call = callData.call + + guard callId == call.signalingId else { + owsFailDebug("received call error for call with id:\(callId) but current call has id:\(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "received call error for call with id:\(callId) but current call has id:\(call.signalingId)")) + return + } + + // We will try to send a hangup over signaling to let the + // remote peer know we are ending the call. + do { + let hangupBuilder = SSKProtoCallMessageHangup.builder(id: call.signalingId) + let callMessage = OWSOutgoingCallMessage(thread: call.thread, hangupMessage: try hangupBuilder.build()) + let sendPromise = messageSender.sendMessage(.promise, callMessage.asPreparer) + .done { + Logger.debug("sent hangup call message to \(call.thread.contactAddress)") + + // We fail the call rather than terminate it. + self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: error)) + }.catch { error in + Logger.error("failed to send hangup call message to \(call.thread.contactAddress) with error: \(error)") + self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "failed to send hangup call message")) + } + + sendPromise.retainUntilComplete() + } catch { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build hangup proto")) + } } - /** - * The connection failed to establish. The clients will not be able to communicate. - */ - internal func peerConnectionClientIceFailed(_ peerConnectionClient: PeerConnectionClient) { + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onAddRemoteVideoTrack track: RTCVideoTrack, callId: UInt64) { AssertIsOnMainThread() guard peerConnectionClient == self.peerConnectionClient else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") + Logger.debug("Ignoring event from obsolete client") return } - // Return to a known good state. - self.handleFailedCurrentCall(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) { - AssertIsOnMainThread() - - guard peerConnectionClient == self.peerConnectionClient else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") + guard let callData = self.callData else { + Logger.debug("Ignoring event from obsolete call") return } - self.handleLocalAddedIceCandidate(iceCandidate) - } + let call = callData.call - /** - * Once the peerConnection is established, we can receive messages via the data channel, and notify the delegate. - */ - internal func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, received dataChannelMessage: WebRTCProtoData) { - AssertIsOnMainThread() - - guard peerConnectionClient == self.peerConnectionClient else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") + guard callId == call.signalingId else { + owsFailDebug("received remote video track for call with id:\(callId) but current call has id:\(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "received remote video track for call with id:\(callId) but current call has id:\(call.signalingId)")) return } - self.handleDataChannelMessage(dataChannelMessage) - } - - internal func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, didUpdateLocalVideoCaptureSession captureSession: AVCaptureSession?) { - AssertIsOnMainThread() - - guard peerConnectionClient == self.peerConnectionClient else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") - return - } - guard let callData = callData else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") - return - } - - callData.localCaptureSession = captureSession + callData.remoteVideoTrack = track fireDidUpdateVideoTracks() } - internal func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, didUpdateRemoteVideoTrack videoTrack: RTCVideoTrack?) { + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onUpdateLocalVideoSession session: AVCaptureSession?, callId: UInt64) { AssertIsOnMainThread() guard peerConnectionClient == self.peerConnectionClient else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") - return - } - guard let callData = callData else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") + Logger.debug("Ignoring event from obsolete client") return } - callData.remoteVideoTrack = videoTrack + guard let callData = self.callData else { + Logger.debug("Ignoring event from obsolete call") + return + } + + let call = callData.call + + guard callId == call.signalingId else { + owsFailDebug("received local video session for call with id:\(callId) but current call has id:\(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "received local video session for call with id:\(callId) but current call has id:\(call.signalingId)")) + return + } + + callData.localCaptureSession = session fireDidUpdateVideoTracks() } + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendOffer sdp: String, callId: UInt64) { + AssertIsOnMainThread() + + Logger.debug("Got onSendOffer") + + guard peerConnectionClient == self.peerConnectionClient else { + Logger.debug("Ignoring event from obsolete client") + return + } + + guard let callData = self.callData else { + Logger.debug("Ignoring event from obsolete call") + return + } + + let call = callData.call + + guard callId == call.signalingId else { + owsFailDebug("should send offer for call with id:\(callId) but current call has id:\(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "should send offer for call with id:\(callId) but current call has id:\(call.signalingId)")) + return + } + + do { + let offerBuilder = SSKProtoCallMessageOffer.builder(id: call.signalingId, sessionDescription: sdp) + let callMessage = OWSOutgoingCallMessage(thread: call.thread, offerMessage: try offerBuilder.build()) + let sendPromise = messageSender.sendMessage(.promise, callMessage.asPreparer) + .done { + Logger.debug("sent offer call message to \(call.thread.contactAddress)") + + // Ultimately, RingRTC will start sending Ice Candidates at the proper + // time, so we open the gate here right after sending the offer. + self.readyToSendIceUpdates(call: call) + }.catch { error in + Logger.error("failed to send offer call message to \(call.thread.contactAddress) with error: \(error)") + self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "failed to send offer call message")) + } + + sendPromise.retainUntilComplete() + } catch { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build offer proto")) + } + } + + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendAnswer sdp: String, callId: UInt64) { + AssertIsOnMainThread() + + Logger.debug("Got onSendAnswer") + + guard peerConnectionClient == self.peerConnectionClient else { + Logger.debug("Ignoring event from obsolete client") + return + } + + guard let callData = self.callData else { + Logger.debug("Ignoring event from obsolete call") + return + } + + let call = callData.call + + guard callId == call.signalingId else { + owsFailDebug("should send answer for call with id:\(callId) but current call has id:\(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "should send answer for call with id:\(callId) but current call has id:\(call.signalingId)")) + return + } + + do { + let answerBuilder = SSKProtoCallMessageAnswer.builder(id: call.signalingId, sessionDescription: sdp) + let callMessage = OWSOutgoingCallMessage(thread: call.thread, answerMessage: try answerBuilder.build()) + let sendPromise = messageSender.sendMessage(.promise, callMessage.asPreparer) + .done { + Logger.debug("sent answer call message to \(call.thread.contactAddress)") + + // Ultimately, RingRTC will start sending Ice Candidates at the proper + // time, so we open the gate here right after sending the answer. + self.readyToSendIceUpdates(call: call) + }.catch { error in + Logger.error("failed to send answer call message to \(call.thread.contactAddress) with error: \(error)") + self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "failed to send answer call message")) + } + + sendPromise.retainUntilComplete() + } catch { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build answer proto")) + } + } + + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendIceCandidates candidates: [RTCIceCandidate], callId: UInt64) { + AssertIsOnMainThread() + + guard peerConnectionClient == self.peerConnectionClient else { + Logger.debug("Ignoring event from obsolete client") + return + } + + guard let callData = self.callData else { + Logger.debug("Ignoring event from obsolete call") + return + } + + let call = callData.call + + guard callId == call.signalingId else { + owsFailDebug("should send ice candidate for call with id:\(callId) but current call has id:\(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "should send ice candidate for call with id:\(callId) but current call has id:\(call.signalingId)")) + return + } + + // We keep the Ice Candidate queue here in the app so that + // it can batch candidates together as fast as it takes to + // actually send the messages. + for iceCandidate in candidates { + self.handleLocalAddedIceCandidate(iceCandidate, callData: callData) + } + } + + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendHangup callId: UInt64) { + AssertIsOnMainThread() + + Logger.debug("Got onSendHangup") + + guard peerConnectionClient == self.peerConnectionClient else { + Logger.debug("Ignoring event from obsolete client") + return + } + + guard let callData = self.callData else { + Logger.debug("Ignoring event from obsolete call") + return + } + + let call = callData.call + + guard callId == call.signalingId else { + owsFailDebug("should send hangup for call with id:\(callId) but current call has id:\(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "should send hangup for call with id:\(callId) but current call has id:\(call.signalingId)")) + return + } + + do { + let hangupBuilder = SSKProtoCallMessageHangup.builder(id: call.signalingId) + let callMessage = OWSOutgoingCallMessage(thread: call.thread, hangupMessage: try hangupBuilder.build()) + let sendPromise = messageSender.sendMessage(.promise, callMessage.asPreparer) + .done { + Logger.debug("sent hangup call message to \(call.thread.contactAddress)") + + self.terminateCall() + }.catch { error in + Logger.error("failed to send hangup call message to \(call.thread.contactAddress) with error: \(error)") + self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "failed to send hangup call message")) + } + + sendPromise.retainUntilComplete() + } catch { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build hangup proto")) + } + } + // MARK: - /** @@ -1684,9 +1701,18 @@ private class SignalCallData: NSObject { // * If we know which call it was, we should update that call's state // to reflect the error. // * IFF that call is the current call, we want to terminate it. - public func handleFailedCall(failedCall: SignalCall?, error: CallError) { + public func handleFailedCall(failedCall: SignalCall?, error: Error) { AssertIsOnMainThread() + let callError: CallError = { + switch error { + case let callError as CallError: + return callError + default: + return CallError.externalError(underlyingError: error) + } + }() + if case CallError.assertionError(description: let description) = error { owsFailDebug(description) } @@ -1703,9 +1729,9 @@ private class SignalCallData: NSObject { } // It's essential to set call.state before terminateCall, because terminateCall nils self.call - failedCall.error = error + failedCall.error = callError failedCall.state = .localFailure - self.callUIAdapter.failCall(failedCall, error: error) + self.callUIAdapter.failCall(failedCall, error: callError) // Only terminate the current call if the error pertains to the current call. guard failedCall == self.call else { @@ -1797,32 +1823,20 @@ private class SignalCallData: NSObject { private func updateIsVideoEnabled() { AssertIsOnMainThread() - guard let call = self.call else { - return - } guard let peerConnectionClient = self.peerConnectionClient else { return } let shouldHaveLocalVideoTrack = self.shouldHaveLocalVideoTrack() - Logger.info("\(shouldHaveLocalVideoTrack)") + Logger.info("shouldHaveLocalVideoTrack: \(shouldHaveLocalVideoTrack)") - self.peerConnectionClient?.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack) - - var messageData: Data do { - let videoStreamingStatusBuilder = WebRTCProtoVideoStreamingStatus.builder(id: call.signalingId) - videoStreamingStatusBuilder.setEnabled(shouldHaveLocalVideoTrack) - let dataBuilder = WebRTCProtoData.builder() - dataBuilder.setVideoStreamingStatus(try videoStreamingStatusBuilder.build()) - messageData = try dataBuilder.buildSerializedData() + try peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack) + try peerConnectionClient.sendLocalVideoStatus(enabled: shouldHaveLocalVideoTrack) } catch { - Logger.error("couldn't build proto") - return + Logger.error("error: \(error)") } - - peerConnectionClient.sendDataChannelMessage(data: messageData, description: "videoStreamingStatus", isCritical: false) } // MARK: - Observers @@ -1922,7 +1936,6 @@ private class SignalCallData: NSObject { } if !OWSWindowManager.shared().hasCall() { - OWSProdError(OWSAnalyticsEvents.callServiceCallViewCouldNotPresent(), file: #file, function: #function, line: #line) owsFailDebug("Call terminated due to missing call view.") self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "Call view didn't present after \(kMaxViewPresentationDelay) seconds")) return diff --git a/Signal/src/call/PeerConnectionClient.swift b/Signal/src/call/PeerConnectionClient.swift index 18319df21d..f654ff4ff6 100644 --- a/Signal/src/call/PeerConnectionClient.swift +++ b/Signal/src/call/PeerConnectionClient.swift @@ -4,15 +4,9 @@ import Foundation import PromiseKit +import SignalCoreKit +import SignalRingRTC import WebRTC -import SignalServiceKit -import SignalMessaging - -// HACK - Seeing crazy SEGFAULTs on iOS9 when accessing these objc externs. -// iOS10 seems unaffected. Reproducible for ~1 in 3 calls. -// Binding them to a file constant seems to work around the problem. -let kAudioTrackType = kRTCMediaStreamTrackKindAudio -let kVideoTrackType = kRTCMediaStreamTrackKindVideo /** * The PeerConnectionClient notifies it's delegate (the CallService) of key events in the call signaling life cycle @@ -22,44 +16,44 @@ let kVideoTrackType = kRTCMediaStreamTrackKindVideo protocol PeerConnectionClientDelegate: class { /** - * The connection has been established. The clients can now communicate. - * This can be called multiple times throughout the call in the event of temporary network disconnects. + * Fired for various asynchronous RingRTC events. See CallConnection.CallEvent for more information. */ - func peerConnectionClientIceConnected(_ peerconnectionClient: PeerConnectionClient) + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onCallEvent event: CallEvent, callId: UInt64) /** - * The connection failed to establish. The clients will not be able to communicate. + * Fired whenever RingRTC encounters an error. Should always be considered fatal and end the session. */ - func peerConnectionClientIceFailed(_ peerconnectionClient: PeerConnectionClient) + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onCallError error: String, callId: UInt64) /** - * After initially connecting, the connection disconnected. - * It maybe be temporary, in which case `peerConnectionClientIceConnected` will be called again once we're reconnected. - * Otherwise, `peerConnectionClientIceFailed` will eventually called. + * Fired whenever the remote video track becomes active or inactive. */ - func peerConnectionClientIceDisconnected(_ peerconnectionClient: PeerConnectionClient) + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onAddRemoteVideoTrack track: RTCVideoTrack, callId: UInt64) /** - * 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. + * Fired whenever the local video track becomes active or inactive. */ - func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, addedLocalIceCandidate iceCandidate: RTCIceCandidate) + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onUpdateLocalVideoSession session: AVCaptureSession?, callId: UInt64) /** - * Once the peerconnection is established, we can receive messages via the data channel, and notify the delegate. + * Fired when an offer message should be sent over the signaling channel. */ - func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, received dataChannelMessage: WebRTCProtoData) + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendOffer sdp: String, callId: UInt64) /** - * Fired whenever the local video track become active or inactive. + * Fired when an answer message should be sent over the signaling channel. */ - func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, didUpdateLocalVideoCaptureSession captureSession: AVCaptureSession?) + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendAnswer sdp: String, callId: UInt64) /** - * Fired whenever the remote video track become active or inactive. + * Fired when there are one or more local Ice Candidates to be sent over the signaling channel. */ - func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, didUpdateRemoteVideoTrack videoTrack: RTCVideoTrack?) + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendIceCandidates candidates: [RTCIceCandidate], callId: UInt64) + + /** + * Fired when a hangup message should be sent over the signaling channel. + */ + func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendHangup callId: UInt64) } // In Swift (at least in Swift v3.3), weak variables aren't thread safe. It @@ -93,8 +87,7 @@ protocol PeerConnectionClientDelegate: class { // * Alice or Bob (randomly alternating) calls the other. Recipient (randomly) // accepts call or hangs up. If accepted, Alice or Bob (randomly) hangs up. // Repeat immediately, as fast as you can, 10-20x. -class PeerConnectionProxy: NSObject, RTCPeerConnectionDelegate, RTCDataChannelDelegate { - +class PeerConnectionProxy: NSObject, CallConnectionDelegate { private var value: PeerConnectionClient? deinit { @@ -129,60 +122,42 @@ class PeerConnectionProxy: NSObject, RTCPeerConnectionDelegate, RTCDataChannelDe objc_sync_exit(self) } - // MARK: - RTCPeerConnectionDelegate + // MARK: - CallConnectionDelegate - public func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) { - self.get()?.peerConnection(peerConnection, didChange: stateChanged) + func callConnection(_ callConnection: CallConnection, onCallEvent event: CallEvent, callId: UInt64) { + self.get()?.callConnection(callConnection, onCallEvent: event, callId: callId) } - public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) { - self.get()?.peerConnection(peerConnection, didAdd: stream) + func callConnection(_ callConnection: CallConnection, onCallError error: String, callId: UInt64) { + self.get()?.callConnection(callConnection, onCallError: error, callId: callId) } - public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) { - self.get()?.peerConnection(peerConnection, didRemove: stream) + func callConnection(_ callConnection: CallConnection, onAddRemoteVideoTrack track: RTCVideoTrack, callId: UInt64) { + self.get()?.callConnection(callConnection, onAddRemoteVideoTrack: track, callId: callId) } - public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { - self.get()?.peerConnectionShouldNegotiate(peerConnection) + func callConnection(_ callConnection: CallConnection, onUpdateLocalVideoSession session: AVCaptureSession?, callId: UInt64) { + self.get()?.callConnection(callConnection, onUpdateLocalVideoSession: session, callId: callId) } - public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) { - self.get()?.peerConnection(peerConnection, didChange: newState) + func callConnection(_ callConnection: CallConnection, shouldSendOffer sdp: String, callId: UInt64) { + self.get()?.callConnection(callConnection, shouldSendOffer: sdp, callId: callId) } - public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) { - self.get()?.peerConnection(peerConnection, didChange: newState) + func callConnection(_ callConnection: CallConnection, shouldSendAnswer sdp: String, callId: UInt64) { + self.get()?.callConnection(callConnection, shouldSendAnswer: sdp, callId: callId) } - public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { - self.get()?.peerConnection(peerConnection, didGenerate: candidate) + func callConnection(_ callConnection: CallConnection, shouldSendIceCandidates candidates: [RTCIceCandidate], callId: UInt64) { + self.get()?.callConnection(callConnection, shouldSendIceCandidates: candidates, callId: callId) } - public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) { - self.get()?.peerConnection(peerConnection, didRemove: candidates) + func callConnection(_ callConnection: CallConnection, shouldSendHangup callId: UInt64) { + self.get()?.callConnection(callConnection, shouldSendHangup: callId) } - public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { - self.get()?.peerConnection(peerConnection, didOpen: dataChannel) - } - - public func peerConnection(_ peerConnection: RTCPeerConnection, didChange connectionState: RTCPeerConnectionState) { - self.get()?.peerConnection(peerConnection, didChange: connectionState) - } - - // MARK: - RTCDataChannelDelegate - - public func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) { - self.get()?.dataChannelDidChangeState(dataChannel) - } - - public func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) { - self.get()?.dataChannel(dataChannel, didReceiveMessageWith: buffer) - } - - public func dataChannel(_ dataChannel: RTCDataChannel, didChangeBufferedAmount amount: UInt64) { - self.get()?.dataChannel(dataChannel, didChangeBufferedAmount: amount) + func callConnection(_ callConnection: CallConnection, shouldSendBusy callId: UInt64) { + self.get()?.callConnection(callConnection, shouldSendBusy: callId) } } @@ -192,19 +167,8 @@ class PeerConnectionProxy: NSObject, RTCPeerConnectionDelegate, RTCDataChannelDe * It is primarily a wrapper around `RTCPeerConnection`, which is responsible for sending and receiving our call data * including audio, video, and some post-connected signaling (hangup, add video) */ -class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelDelegate, VideoCaptureSettingsDelegate { +class PeerConnectionClient: NSObject, CallConnectionDelegate { - enum Identifiers: String { - case mediaStream = "ARDAMS", - videoTrack = "ARDAMSv0", - audioTrack = "ARDAMSa0", - dataChannelSignaling = "signaling" - } - - // A state in this class should only be accessed on this queue in order to - // serialize access. - // - // This queue is also used to perform expensive calls to the WebRTC API. private static let signalingQueue = DispatchQueue(label: "CallServiceSignalingQueue") // Delegate is notified of key events in the call lifecycle. @@ -214,515 +178,38 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD // Connection - private var peerConnection: RTCPeerConnection? - private let iceServers: [RTCIceServer] - private let connectionConstraints: RTCMediaConstraints - private let configuration: RTCConfiguration - private let factory: RTCPeerConnectionFactory - - // DataChannel - - private var dataChannel: RTCDataChannel? - - // Audio - - private var audioSender: RTCRtpSender? - private var audioTrack: RTCAudioTrack? - private var audioConstraints: RTCMediaConstraints - - // Video - - private var videoCaptureController: VideoCaptureController? - private var videoSender: RTCRtpSender? - - // RTCVideoTrack is fragile and prone to throwing exceptions and/or - // causing deadlock in its destructor. Therefore we take great care - // with this property. - private var localVideoTrack: RTCVideoTrack? - private var remoteVideoTrack: RTCVideoTrack? - private var cameraConstraints: RTCMediaConstraints + private var callConnectionfactory: CallConnectionFactory? + private var callConnection: CallConnection? private let proxy = PeerConnectionProxy() + // Note that we're deliberately leaking proxy instances using this // collection to avoid EXC_BAD_ACCESS. Calls are rare and the proxy // is tiny (a single property), so it's better to leak and be safe. private static var expiredProxies = [PeerConnectionProxy]() - init(iceServers: [RTCIceServer], delegate: PeerConnectionClientDelegate, callDirection: CallDirection, useTurnOnly: Bool) { + init(delegate: PeerConnectionClientDelegate) { AssertIsOnMainThread() - self.iceServers = iceServers self.delegate = delegate - // Ensure we enable SW decoders to enable VP8 support - let decoderFactory = RTCDefaultVideoDecoderFactory() - let encoderFactory = RTCDefaultVideoEncoderFactory() - let factory = RTCPeerConnectionFactory(encoderFactory: encoderFactory, decoderFactory: decoderFactory) - - self.factory = factory - configuration = RTCConfiguration() - configuration.iceServers = iceServers - configuration.bundlePolicy = .maxBundle - configuration.rtcpMuxPolicy = .require - if useTurnOnly { - Logger.debug("using iceTransportPolicy: relay") - configuration.iceTransportPolicy = .relay - } else { - Logger.debug("using iceTransportPolicy: default") - } - - let connectionConstraintsDict = ["DtlsSrtpKeyAgreement": "true"] - connectionConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: connectionConstraintsDict) - - audioConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) - cameraConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) + callConnectionfactory = CallConnectionFactory() super.init() proxy.set(value: self) - peerConnection = factory.peerConnection(with: configuration, - constraints: connectionConstraints, - delegate: proxy) - createAudioSender() - createVideoSender() - - if callDirection == .outgoing { - // When placing an outgoing call, it's our responsibility to create the DataChannel. - // Recipient will not have to do this explicitly. - createSignalingDataChannel() - } + Logger.debug("object! PeerConnectionClient created \(ObjectIdentifier(self))") } deinit { - // TODO: We can demote this log level to debug once we're confident that - // this class is always deallocated. - Logger.info("[PeerConnectionClient] deinit") + Logger.debug("object! PeerConnectionClient destroyed \(ObjectIdentifier(self))") } - // MARK: - Media Streams - - private func createSignalingDataChannel() { - AssertIsOnMainThread() - guard let peerConnection = peerConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - let configuration = RTCDataChannelConfiguration() - // Insist upon an "ordered" TCP data channel for delivery reliability. - configuration.isOrdered = true - - guard let dataChannel = peerConnection.dataChannel(forLabel: Identifiers.dataChannelSignaling.rawValue, - configuration: configuration) else { - - // TODO fail outgoing call? - owsFailDebug("dataChannel was unexpectedly nil") - return - } - dataChannel.delegate = proxy - - assert(self.dataChannel == nil) - self.dataChannel = dataChannel - } - - // MARK: - Video - - fileprivate func createVideoSender() { - AssertIsOnMainThread() - Logger.debug("") - assert(self.videoSender == nil, "\(#function) should only be called once.") - - guard !Platform.isSimulator else { - Logger.warn("Refusing to create local video track on simulator which has no capture device.") - return - } - guard let peerConnection = peerConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - let videoSource = factory.videoSource() - - let localVideoTrack = factory.videoTrack(with: videoSource, trackId: Identifiers.videoTrack.rawValue) - self.localVideoTrack = localVideoTrack - // Disable by default until call is connected. - // FIXME - do we require mic permissions at this point? - // if so maybe it would be better to not even add the track until the call is connected - // instead of creating it and disabling it. - localVideoTrack.isEnabled = false - - let capturer = RTCCameraVideoCapturer(delegate: videoSource) - self.videoCaptureController = VideoCaptureController(capturer: capturer, settingsDelegate: self) - - let videoSender = peerConnection.sender(withKind: kVideoTrackType, streamId: Identifiers.mediaStream.rawValue) - videoSender.track = localVideoTrack - self.videoSender = videoSender - } - - public func setCameraSource(isUsingFrontCamera: Bool) { + public func close() { AssertIsOnMainThread() - let proxyCopy = self.proxy - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { return } - - guard let captureController = strongSelf.videoCaptureController else { - owsFailDebug("captureController was unexpectedly nil") - return - } - - captureController.switchCamera(isUsingFrontCamera: isUsingFrontCamera) - } - } - - public func setLocalVideoEnabled(enabled: Bool) { - AssertIsOnMainThread() - let proxyCopy = self.proxy - let completion = { - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - - let captureSession: AVCaptureSession? = { - guard enabled else { - return nil - } - - guard let captureController = strongSelf.videoCaptureController else { - owsFailDebug("videoCaptureController was unexpectedly nil") - return nil - } - - return captureController.captureSession - }() - - strongDelegate.peerConnectionClient(strongSelf, didUpdateLocalVideoCaptureSession: captureSession) - } - - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { return } - guard strongSelf.peerConnection != nil else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - guard let videoCaptureController = strongSelf.videoCaptureController else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - guard let localVideoTrack = strongSelf.localVideoTrack else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - localVideoTrack.isEnabled = enabled - - if enabled { - Logger.debug("starting video capture") - videoCaptureController.startCapture() - } else { - Logger.debug("stopping video capture") - videoCaptureController.stopCapture() - } - - DispatchQueue.main.async(execute: completion) - } - } - - // MARK: VideoCaptureSettingsDelegate - - var videoWidth: Int32 { - return 400 - } - - var videoHeight: Int32 { - return 400 - } - - // MARK: - Audio - - fileprivate func createAudioSender() { - AssertIsOnMainThread() - Logger.debug("") - assert(self.audioSender == nil, "\(#function) should only be called once.") - - guard let peerConnection = peerConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - let audioSource = factory.audioSource(with: self.audioConstraints) - - let audioTrack = factory.audioTrack(with: audioSource, trackId: Identifiers.audioTrack.rawValue) - self.audioTrack = audioTrack - - // Disable by default until call is connected. - // FIXME - do we require mic permissions at this point? - // if so maybe it would be better to not even add the track until the call is connected - // instead of creating it and disabling it. - audioTrack.isEnabled = false - - let audioSender = peerConnection.sender(withKind: kAudioTrackType, streamId: Identifiers.mediaStream.rawValue) - audioSender.track = audioTrack - self.audioSender = audioSender - } - - public func setAudioEnabled(enabled: Bool) { - AssertIsOnMainThread() - let proxyCopy = self.proxy - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { return } - guard strongSelf.peerConnection != nil else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - guard let audioTrack = strongSelf.audioTrack else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - audioTrack.isEnabled = enabled - } - } - - // MARK: - Session negotiation - - private var defaultOfferConstraints: RTCMediaConstraints { - let mandatoryConstraints = [ - "OfferToReceiveAudio": "true", - "OfferToReceiveVideo": "true" - ] - return RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints: nil) - } - - public func createOffer() -> Promise { - AssertIsOnMainThread() - let proxyCopy = self.proxy - let (promise, resolver) = Promise.pending() - let completion: ((RTCSessionDescription?, Error?) -> Void) = { (sdp, error) in - guard let strongSelf = proxyCopy.get() else { - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - strongSelf.assertOnSignalingQueue() - guard strongSelf.peerConnection != nil else { - Logger.debug("Ignoring obsolete event in terminated client") - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - if let error = error { - resolver.reject(error) - return - } - - guard let sessionDescription = sdp else { - Logger.error("No session description was obtained, even though there was no error reported.") - let error = OWSErrorMakeUnableToProcessServerResponseError() - resolver.reject(error) - return - } - - resolver.fulfill(HardenedRTCSessionDescription(rtcSessionDescription: sessionDescription)) - } - - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - strongSelf.assertOnSignalingQueue() - guard let peerConnection = strongSelf.peerConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - - peerConnection.offer(for: strongSelf.defaultOfferConstraints, completionHandler: { (sdp: RTCSessionDescription?, error: Error?) in - PeerConnectionClient.signalingQueue.async { - completion(sdp, error) - } - }) - } - - return promise - } - - public func setLocalSessionDescriptionInternal(_ sessionDescription: HardenedRTCSessionDescription) -> Promise { - let proxyCopy = self.proxy - let (promise, resolver) = Promise.pending() - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - strongSelf.assertOnSignalingQueue() - - guard let peerConnection = strongSelf.peerConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - - Logger.verbose("setting local session description: \(sessionDescription)") - peerConnection.setLocalDescription(sessionDescription.rtcSessionDescription, completionHandler: { (error) in - if let error = error { - resolver.reject(error) - } else { - resolver.fulfill(()) - } - }) - } - return promise - } - - public func setLocalSessionDescription(_ sessionDescription: HardenedRTCSessionDescription) -> Promise { - AssertIsOnMainThread() - let proxyCopy = self.proxy - let (promise, resolver) = Promise.pending() - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - strongSelf.assertOnSignalingQueue() - guard let peerConnection = strongSelf.peerConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - - Logger.verbose("setting local session description: \(sessionDescription)") - peerConnection.setLocalDescription(sessionDescription.rtcSessionDescription, - completionHandler: { error in - if let error = error { - resolver.reject(error) - return - } - resolver.fulfill(()) - }) - } - - return promise - } - - public func negotiateSessionDescription(remoteDescription: RTCSessionDescription, constraints: RTCMediaConstraints) -> Promise { - AssertIsOnMainThread() - let proxyCopy = self.proxy - return setRemoteSessionDescription(remoteDescription) - .then(on: PeerConnectionClient.signalingQueue) { _ -> Promise in - guard let strongSelf = proxyCopy.get() else { - return Promise(error: NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - } - return strongSelf.negotiateAnswerSessionDescription(constraints: constraints) - } - } - - public func setRemoteSessionDescription(_ sessionDescription: RTCSessionDescription) -> Promise { - AssertIsOnMainThread() - let proxyCopy = self.proxy - let (promise, resolver) = Promise.pending() - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - strongSelf.assertOnSignalingQueue() - guard let peerConnection = strongSelf.peerConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - Logger.verbose("setting remote description: \(sessionDescription)") - peerConnection.setRemoteDescription(sessionDescription, - completionHandler: { error in - if let error = error { - resolver.reject(error) - return - } - resolver.fulfill(()) - }) - } - return promise - } - - private func negotiateAnswerSessionDescription(constraints: RTCMediaConstraints) -> Promise { - assertOnSignalingQueue() - let proxyCopy = self.proxy - let (promise, resolver) = Promise.pending() - let completion: ((RTCSessionDescription?, Error?) -> Void) = { (sdp, error) in - guard let strongSelf = proxyCopy.get() else { - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - strongSelf.assertOnSignalingQueue() - guard strongSelf.peerConnection != nil else { - Logger.debug("Ignoring obsolete event in terminated client") - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - if let error = error { - resolver.reject(error) - return - } - - guard let sessionDescription = sdp else { - Logger.error("unexpected empty session description, even though no error was reported.") - let error = OWSErrorMakeUnableToProcessServerResponseError() - resolver.reject(error) - return - } - - let hardenedSessionDescription = HardenedRTCSessionDescription(rtcSessionDescription: sessionDescription) - - firstly { - strongSelf.setLocalSessionDescriptionInternal(hardenedSessionDescription) - }.done(on: PeerConnectionClient.signalingQueue) { - resolver.fulfill(hardenedSessionDescription) - }.catch { error in - resolver.reject(error) - }.retainUntilComplete() - } - - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - strongSelf.assertOnSignalingQueue() - - guard let peerConnection = strongSelf.peerConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - resolver.reject(NSError(domain: "Obsolete client", code: 0, userInfo: nil)) - return - } - - Logger.debug("negotiating answer session.") - - peerConnection.answer(for: constraints, completionHandler: { (sdp: RTCSessionDescription?, error: Error?) in - PeerConnectionClient.signalingQueue.async { - completion(sdp, error) - } - }) - } - return promise - } - - public func addRemoteIceCandidate(_ candidate: RTCIceCandidate) { - let proxyCopy = self.proxy - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { return } - guard let peerConnection = strongSelf.peerConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - Logger.info("adding remote ICE candidate: \(candidate.sdp)") - peerConnection.add(candidate) - } - } - - public func terminate() { - AssertIsOnMainThread() - Logger.debug("") + Logger.debug("close") // Clear the delegate immediately so that we can guarantee that // no delegate methods are called after terminate() returns. @@ -735,349 +222,387 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD // Don't use [weak self]; we always want to perform terminateInternal(). PeerConnectionClient.signalingQueue.async { - self.terminateInternal() + // Some notes on preventing crashes while disposing of peerConnection for video calls + // from: https://groups.google.com/forum/#!searchin/discuss-webrtc/objc$20crash$20dealloc%7Csort:relevance/discuss-webrtc/7D-vk5yLjn8/rBW2D6EW4GYJ + // The sequence to make it work appears to be + // + // [capturer stop]; // I had to add this as a method to RTCVideoCapturer + // [localRenderer stop]; + // [remoteRenderer stop]; + // [peerConnection close]; + + if let callConnection = self.callConnection { + Logger.debug("Calling callConnection.close()") + callConnection.close() + } + self.callConnection = nil + Logger.debug("callConnection is nil") + + if let callConnectionfactory = self.callConnectionfactory { + Logger.debug("Calling callConnectionfactory.close()") + callConnectionfactory.close() + } + self.callConnectionfactory = nil + Logger.debug("callConnectionfactory is nil") } } - private func terminateInternal() { - assertOnSignalingQueue() - Logger.debug("") + // MARK: - Session negotiation - // Some notes on preventing crashes while disposing of peerConnection for video calls - // from: https://groups.google.com/forum/#!searchin/discuss-webrtc/objc$20crash$20dealloc%7Csort:relevance/discuss-webrtc/7D-vk5yLjn8/rBW2D6EW4GYJ - // The sequence to make it work appears to be - // - // [capturer stop]; // I had to add this as a method to RTCVideoCapturer - // [localRenderer stop]; - // [remoteRenderer stop]; - // [peerConnection close]; - - // 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 - - localVideoTrack?.isEnabled = false - remoteVideoTrack?.isEnabled = false - - if let dataChannel = self.dataChannel { - dataChannel.delegate = nil - } - - dataChannel = nil - audioSender = nil - audioTrack = nil - videoSender = nil - localVideoTrack = nil - remoteVideoTrack = nil - videoCaptureController = nil - - if let peerConnection = peerConnection { - peerConnection.delegate = nil - peerConnection.close() - } - peerConnection = nil - } - - // MARK: - Data Channel - - // should only be accessed on PeerConnectionClient.signalingQueue - var pendingDataChannelMessages: [PendingDataChannelMessage] = [] - struct PendingDataChannelMessage { - let data: Data - let description: String - let isCritical: Bool - } - - public func sendDataChannelMessage(data: Data, description: String, isCritical: Bool) { + public func sendOffer(iceServers: [RTCIceServer], useTurnOnly: Bool, callId: UInt64) throws { AssertIsOnMainThread() + + Logger.debug("sendOffer") + + guard let callConnectionfactory = self.callConnectionfactory else { + throw CallError.fatalError(description: "Missing factory") + } + + let callConnection = try callConnectionfactory.createCallConnection(delegate: proxy, iceServers: iceServers, callId: callId, isOutgoing: true, hideIp: useTurnOnly) + self.callConnection = callConnection + + try callConnection.sendOffer() + } + + public func receivedOffer(iceServers: [RTCIceServer], useTurnOnly: Bool, callId: UInt64, sdp: String) throws { + AssertIsOnMainThread() + + Logger.debug("receivedOffer") + + guard let callConnectionfactory = self.callConnectionfactory else { + throw CallError.fatalError(description: "Missing factory") + } + + let callConnection = try callConnectionfactory.createCallConnection(delegate: proxy, iceServers: iceServers, callId: callId, isOutgoing: false, hideIp: useTurnOnly) + self.callConnection = callConnection + + try callConnection.receivedOffer(sdp: sdp) + } + + public func receivedAnswer(sdp: String) throws { + AssertIsOnMainThread() + + Logger.debug("receivedAnswer") + + guard let callConnection = self.callConnection else { + throw CallError.obsoleteCall(description: "Invalid callConnection") + } + + try callConnection.receivedAnswer(sdp: sdp) + } + + public func receivedIceCandidate(sdp: String, lineIndex: Int32, sdpMid: String) throws { + AssertIsOnMainThread() + + Logger.debug("receivedIceCandidate") + + guard let callConnection = self.callConnection else { + throw CallError.obsoleteCall(description: "Invalid callConnection") + } + + try callConnection.receivedIceCandidate(sdp: sdp, lineIndex: lineIndex, sdpMid: sdpMid) + } + + // MARK: - Session Control + + public func acceptCall() throws { + AssertIsOnMainThread() + + Logger.debug("acceptCall") + + guard let callConnection = self.callConnection else { + throw CallError.obsoleteCall(description: "Invalid callConnection") + } + + try callConnection.accept() + } + + public func hangup() throws { + AssertIsOnMainThread() + + Logger.debug("hangup") + + guard let callConnection = self.callConnection else { + throw CallError.obsoleteCall(description: "Invalid callConnection") + } + + try callConnection.hangup() + } + + public func setLocalAudioEnabled(enabled: Bool) throws { + AssertIsOnMainThread() + + Logger.debug("setLocalAudioEnabled \(enabled)") + + guard let callConnection = self.callConnection else { + throw CallError.obsoleteCall(description: "Invalid callConnection") + } + + try callConnection.setLocalAudioEnabled(enabled: enabled) + } + + public func setLocalVideoEnabled(enabled: Bool) throws { + AssertIsOnMainThread() + + Logger.debug("setLocalVideoEnabled \(enabled)") + + guard let callConnection = self.callConnection else { + throw CallError.obsoleteCall(description: "Invalid callConnection") + } + + try callConnection.setLocalVideoEnabled(enabled: enabled) + } + + public func sendLocalVideoStatus(enabled: Bool) throws { + AssertIsOnMainThread() + + Logger.debug("sendLocalVideoStatus \(enabled)") + + guard let callConnection = self.callConnection else { + throw CallError.obsoleteCall(description: "Invalid callConnection") + } + + try callConnection.sendLocalVideoStatus(enabled: enabled) + } + + public func setCameraSource(isUsingFrontCamera: Bool) throws { + AssertIsOnMainThread() + + Logger.debug("setCameraSource \(isUsingFrontCamera)") + let proxyCopy = self.proxy + PeerConnectionClient.signalingQueue.async { guard let strongSelf = proxyCopy.get() else { return } - guard strongSelf.peerConnection != nil else { - Logger.debug("Ignoring obsolete event in terminated client: \(description)") - return - } - - guard let dataChannel = strongSelf.dataChannel else { - if isCritical { - Logger.info("enqueuing critical data channel message for after we have a dataChannel: \(description)") - strongSelf.pendingDataChannelMessages.append(PendingDataChannelMessage(data: data, description: description, isCritical: isCritical)) - } else { - Logger.error("ignoring sending \(data) for nil dataChannel: \(description)") - } - return - } - - Logger.debug("sendDataChannelMessage trying: \(description)") - - let buffer = RTCDataBuffer(data: data, isBinary: false) - let result = dataChannel.sendData(buffer) - - if result { - Logger.debug("sendDataChannelMessage succeeded: \(description)") - } else { - Logger.warn("sendDataChannelMessage failed: \(description)") - if isCritical { - OWSProdError(OWSAnalyticsEvents.peerConnectionClientErrorSendDataChannelMessageFailed(), file: #file, function: #function, line: #line) - } - } - } - } - - // MARK: RTCDataChannelDelegate - - /** The data channel state changed. */ - internal func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) { - Logger.debug("dataChannelDidChangeState: \(dataChannel)") - } - - /** The data channel successfully received a data buffer. */ - internal func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) { - let proxyCopy = self.proxy - let completion: (WebRTCProtoData) -> Void = { (dataChannelMessage) in - AssertIsOnMainThread() - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - strongDelegate.peerConnectionClient(strongSelf, received: dataChannelMessage) - } - - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { return } - guard strongSelf.peerConnection != nil else { + guard let callConnection = strongSelf.callConnection else { Logger.debug("Ignoring obsolete event in terminated client") return } - Logger.debug("dataChannel didReceiveMessageWith buffer:\(buffer)") - var dataChannelMessage: WebRTCProtoData do { - dataChannelMessage = try WebRTCProtoData.parseData(buffer.data) + try callConnection.switchCamera(isUsingFrontCamera: isUsingFrontCamera) } catch { - Logger.error("failed to parse dataProto") - return - } - - DispatchQueue.main.async { - completion(dataChannelMessage) + Logger.debug("callConnection.switchCamera failed with \(error)") } } } - /** The data channel's |bufferedAmount| changed. */ - internal func dataChannel(_ dataChannel: RTCDataChannel, didChangeBufferedAmount amount: UInt64) { - Logger.debug("didChangeBufferedAmount: \(amount)") - } + // MARK: - CallConnectionDelegate - // MARK: - RTCPeerConnectionDelegate + internal func callConnection(_ callConnectionParam: CallConnection, onCallEvent event: CallEvent, callId: UInt64) { + Logger.debug("onCallEvent") - /** Called when the SignalingState changed. */ - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didChange stateChanged: RTCSignalingState) { - Logger.debug("didChange signalingState:\(stateChanged.debugDescription)") - } - - /** Called when media is received on a new stream from remote peer. */ - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didAdd stream: RTCMediaStream) { let proxyCopy = self.proxy - let completion: (RTCVideoTrack) -> Void = { (remoteVideoTrack) in - AssertIsOnMainThread() + + DispatchQueue.main.async { + Logger.debug("onCallEvent - main thread") + guard let strongSelf = proxyCopy.get() else { return } guard let strongDelegate = strongSelf.delegate else { return } - // TODO: Consider checking for termination here. - - strongDelegate.peerConnectionClient(strongSelf, didUpdateRemoteVideoTrack: remoteVideoTrack) - } - - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { return } - guard let peerConnection = strongSelf.peerConnection else { + guard let callConnection = strongSelf.callConnection else { Logger.debug("Ignoring obsolete event in terminated client") return } - guard peerConnection == peerConnectionParam else { - owsFailDebug("mismatched peerConnection callback.") + + guard callConnection == callConnectionParam else { + owsFailDebug("Mismatched callConnection callback") return } - guard stream.videoTracks.count > 0 else { - owsFailDebug("didAdd stream missing stream.") - return - } - let remoteVideoTrack = stream.videoTracks[0] - Logger.debug("didAdd stream:\(stream) video tracks: \(stream.videoTracks.count) audio tracks: \(stream.audioTracks.count)") - strongSelf.remoteVideoTrack = remoteVideoTrack - - DispatchQueue.main.async { - completion(remoteVideoTrack) - } + strongDelegate.peerConnectionClient(strongSelf, onCallEvent: event, callId: callId) } } - /** Called when a remote peer closes a stream. */ - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didRemove stream: RTCMediaStream) { - Logger.debug("didRemove Stream:\(stream)") - } + internal func callConnection(_ callConnectionParam: CallConnection, onCallError error: String, callId: UInt64) { + Logger.debug("onCallError") - /** Called when negotiation is needed, for example ICE has restarted. */ - internal func peerConnectionShouldNegotiate(_ peerConnectionParam: RTCPeerConnection) { - Logger.debug("shouldNegotiate") - } - - /** Called any time the IceConnectionState changes. */ - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didChange newState: RTCIceConnectionState) { let proxyCopy = self.proxy - let connectedCompletion : () -> Void = { - AssertIsOnMainThread() - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - strongDelegate.peerConnectionClientIceConnected(strongSelf) - } - let failedCompletion : () -> Void = { - AssertIsOnMainThread() - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - strongDelegate.peerConnectionClientIceFailed(strongSelf) - } - let disconnectedCompletion : () -> Void = { - AssertIsOnMainThread() - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - strongDelegate.peerConnectionClientIceDisconnected(strongSelf) - } - PeerConnectionClient.signalingQueue.async { + DispatchQueue.main.async { + Logger.debug("onCallError - main thread") + guard let strongSelf = proxyCopy.get() else { return } - guard let peerConnection = strongSelf.peerConnection else { + guard let strongDelegate = strongSelf.delegate else { return } + + guard let callConnection = strongSelf.callConnection else { Logger.debug("Ignoring obsolete event in terminated client") return } - guard peerConnection == peerConnectionParam else { - owsFailDebug("mismatched peerConnection callback.") + + guard callConnection == callConnectionParam else { + owsFailDebug("Mismatched callConnection callback") return } - Logger.info("didChange IceConnectionState:\(newState.debugDescription)") - switch newState { - case .connected, .completed: - DispatchQueue.main.async(execute: connectedCompletion) - case .failed: - Logger.warn("RTCIceConnection failed.") - DispatchQueue.main.async(execute: failedCompletion) - case .disconnected: - Logger.warn("RTCIceConnection disconnected.") - DispatchQueue.main.async(execute: disconnectedCompletion) - default: - Logger.debug("ignoring change IceConnectionState:\(newState.debugDescription)") - } + strongDelegate.peerConnectionClient(strongSelf, onCallError: error, callId: callId) } } - /** Called any time the IceGatheringState changes. */ - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didChange newState: RTCIceGatheringState) { - Logger.info("didChange IceGatheringState:\(newState.debugDescription)") - } + internal func callConnection(_ callConnectionParam: CallConnection, onAddRemoteVideoTrack track: RTCVideoTrack, callId: UInt64) { + Logger.debug("onAddRemoteVideoTrack") - /** New ice candidate has been found. */ - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { let proxyCopy = self.proxy - let completion: (RTCIceCandidate) -> Void = { (candidate) in - AssertIsOnMainThread() + + DispatchQueue.main.async { + Logger.debug("onAddRemoteVideoTrack - main thread") + guard let strongSelf = proxyCopy.get() else { return } guard let strongDelegate = strongSelf.delegate else { return } - strongDelegate.peerConnectionClient(strongSelf, addedLocalIceCandidate: candidate) - } - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { return } - guard let peerConnection = strongSelf.peerConnection else { + guard let callConnection = strongSelf.callConnection else { Logger.debug("Ignoring obsolete event in terminated client") return } - guard peerConnection == peerConnectionParam else { - owsFailDebug("mismatched peerConnection callback.") + + guard callConnection == callConnectionParam else { + owsFailDebug("Mismatched callConnection callback") return } - Logger.info("adding local ICE candidate:\(candidate.sdp)") - DispatchQueue.main.async { - completion(candidate) - } + + strongDelegate.peerConnectionClient(strongSelf, onAddRemoteVideoTrack: track, callId: callId) } } - /** Called when a group of local Ice candidates have been removed. */ - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) { - Logger.debug("didRemove IceCandidates:\(candidates)") - } + internal func callConnection(_ callConnectionParam: CallConnection, onUpdateLocalVideoSession session: AVCaptureSession?, callId: UInt64) { + Logger.debug("onUpdateLocalVideoSession") - /** New data channel has been opened. */ - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { let proxyCopy = self.proxy - let completion: ([PendingDataChannelMessage]) -> Void = { (pendingMessages) in - AssertIsOnMainThread() - guard let strongSelf = proxyCopy.get() else { return } - pendingMessages.forEach { message in - strongSelf.sendDataChannelMessage(data: message.data, description: message.description, isCritical: message.isCritical) - } - } - PeerConnectionClient.signalingQueue.async { + DispatchQueue.main.async { + Logger.debug("onUpdateLocalVideoSession - main thread") + guard let strongSelf = proxyCopy.get() else { return } - guard let peerConnection = strongSelf.peerConnection else { + guard let strongDelegate = strongSelf.delegate else { return } + + guard let callConnection = strongSelf.callConnection else { Logger.debug("Ignoring obsolete event in terminated client") return } - guard peerConnection == peerConnectionParam else { - owsFailDebug("mismatched peerConnection callback.") + + guard callConnection == callConnectionParam else { + owsFailDebug("Mismatched callConnection callback") return } - Logger.info("didOpen dataChannel:\(dataChannel)") - if strongSelf.dataChannel != nil { - owsFailDebug("dataChannel unexpectedly set twice.") - } - strongSelf.dataChannel = dataChannel - dataChannel.delegate = strongSelf.proxy - let pendingMessages = strongSelf.pendingDataChannelMessages - strongSelf.pendingDataChannelMessages = [] - DispatchQueue.main.async { - completion(pendingMessages) - } + strongDelegate.peerConnectionClient(strongSelf, onUpdateLocalVideoSession: session, callId: callId) } } - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didChange connectionState: RTCPeerConnectionState) { - Logger.info("didChange PeerConnectionState:\(connectionState.debugDescription)") + internal func callConnection(_ callConnectionParam: CallConnection, shouldSendOffer sdp: String, callId: UInt64) { + Logger.debug("shouldSendOffer") + + let proxyCopy = self.proxy + + DispatchQueue.main.async { + Logger.debug("shouldSendOffer - main thread") + + guard let strongSelf = proxyCopy.get() else { return } + guard let strongDelegate = strongSelf.delegate else { return } + + guard let callConnection = strongSelf.callConnection else { + Logger.debug("Ignoring obsolete event in terminated client") + return + } + + guard callConnection == callConnectionParam else { + owsFailDebug("Mismatched callConnection callback") + return + } + + strongDelegate.peerConnectionClient(strongSelf, shouldSendOffer: sdp, callId: callId) + } } - // MARK: Helpers + internal func callConnection(_ callConnectionParam: CallConnection, shouldSendAnswer sdp: String, callId: UInt64) { + Logger.debug("shouldSendAnswer") - /** - * We synchronize access to state in this class using this queue. - */ - private func assertOnSignalingQueue() { - assertOnQueue(type(of: self).signalingQueue) + let proxyCopy = self.proxy + + DispatchQueue.main.async { + Logger.debug("shouldSendAnswer - main thread") + + guard let strongSelf = proxyCopy.get() else { return } + guard let strongDelegate = strongSelf.delegate else { return } + + guard let callConnection = strongSelf.callConnection else { + Logger.debug("Ignoring obsolete event in terminated client") + return + } + + guard callConnection == callConnectionParam else { + owsFailDebug("Mismatched callConnection callback") + return + } + + strongDelegate.peerConnectionClient(strongSelf, shouldSendAnswer: sdp, callId: callId) + } + } + + internal func callConnection(_ callConnectionParam: CallConnection, shouldSendIceCandidates candidates: [RTCIceCandidate], callId: UInt64) { + Logger.debug("shouldSendIceCandidates") + + let proxyCopy = self.proxy + + DispatchQueue.main.async { + Logger.debug("shouldSendIceCandidates - main thread") + + guard let strongSelf = proxyCopy.get() else { return } + guard let strongDelegate = strongSelf.delegate else { return } + + guard let callConnection = strongSelf.callConnection else { + Logger.debug("Ignoring obsolete event in terminated client") + return + } + + guard callConnection == callConnectionParam else { + owsFailDebug("Mismatched callConnection callback") + return + } + + strongDelegate.peerConnectionClient(strongSelf, shouldSendIceCandidates: candidates, callId: callId) + } + } + + internal func callConnection(_ callConnectionParam: CallConnection, shouldSendHangup callId: UInt64) { + Logger.debug("shouldSendHangup") + + let proxyCopy = self.proxy + + DispatchQueue.main.async { + Logger.debug("shouldSendHangup - main thread") + + guard let strongSelf = proxyCopy.get() else { return } + guard let strongDelegate = strongSelf.delegate else { return } + + guard let callConnection = strongSelf.callConnection else { + Logger.debug("Ignoring obsolete event in terminated client") + return + } + + guard callConnection == callConnectionParam else { + owsFailDebug("Mismatched callConnection callback") + return + } + + strongDelegate.peerConnectionClient(strongSelf, shouldSendHangup: callId) + } + } + + internal func callConnection(_ callConnectionParam: CallConnection, shouldSendBusy callId: UInt64) { + // Not supported on iOS. CallService maintains the state necessary + // to send Busy messages when appropriate. } // MARK: Test-only accessors - internal func peerConnectionForTests() -> RTCPeerConnection { + internal func peerConnectionForTests() -> CallConnection { AssertIsOnMainThread() - var result: RTCPeerConnection? + var result: CallConnection? PeerConnectionClient.signalingQueue.sync { - result = peerConnection - Logger.info("") - } - return result! - } - - internal func dataChannelForTests() -> RTCDataChannel { - AssertIsOnMainThread() - - var result: RTCDataChannel? - PeerConnectionClient.signalingQueue.sync { - result = dataChannel + result = callConnection Logger.info("") } return result! @@ -1091,296 +616,3 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD } } } - -/** - * Restrict an RTCSessionDescription to more secure parameters - */ -class HardenedRTCSessionDescription { - let rtcSessionDescription: RTCSessionDescription - var sdp: String { return rtcSessionDescription.sdp } - - init(rtcSessionDescription: RTCSessionDescription) { - self.rtcSessionDescription = HardenedRTCSessionDescription.harden(rtcSessionDescription: rtcSessionDescription) - } - - /** - * Set some more secure parameters for the session description - */ - class func harden(rtcSessionDescription: RTCSessionDescription) -> RTCSessionDescription { - var description = rtcSessionDescription.sdp - - // Enforce Constant bit rate. - let cbrRegex = try! NSRegularExpression(pattern: "(a=fmtp:111 ((?!cbr=).)*)\r?\n", options: .caseInsensitive) - description = cbrRegex.stringByReplacingMatches(in: description, options: [], range: NSRange(location: 0, length: description.utf16.count), withTemplate: "$1;cbr=1\r\n") - - // Strip plaintext audio-level details - // https://tools.ietf.org/html/rfc6464 - let audioLevelRegex = try! NSRegularExpression(pattern: ".+urn:ietf:params:rtp-hdrext:ssrc-audio-level.*\r?\n", options: .caseInsensitive) - description = audioLevelRegex.stringByReplacingMatches(in: description, options: [], range: NSRange(location: 0, length: description.utf16.count), withTemplate: "") - - return RTCSessionDescription.init(type: rtcSessionDescription.type, sdp: description) - } - - var logSafeDescription: String { - #if DEBUG - return sdp - #else - return redactIPV6(sdp: redactIcePwd(sdp: sdp)) - #endif - } - - private func redactIcePwd(sdp: String) -> String { - #if DEBUG - return sdp - #else - var text = sdp - text = text.replacingOccurrences(of: "\r", with: "\n") - text = text.replacingOccurrences(of: "\n\n", with: "\n") - let lines = text.components(separatedBy: "\n") - let filteredLines: [String] = lines.map { line in - guard !line.contains("ice-pwd") else { - return "[ REDACTED ice-pwd ]" - } - return line - } - let filteredText = filteredLines.joined(separator: "\n") - return filteredText - #endif - } - - private func redactIPV6(sdp: String) -> String { - #if DEBUG - return sdp - #else - - // Example values to match: - // - // * 2001:0db8:85a3:0000:0000:8a2e:0370:7334 - // * 2001:db8:85a3::8a2e:370:7334 - // * ::1 - // * :: - // * ::ffff:192.0.2.128 - // - // See: https://en.wikipedia.org/wiki/IPv6_addresshttps://en.wikipedia.org/wiki/IPv6_address - do { - let regex = try NSRegularExpression(pattern: "[\\da-f]*:[\\da-f]*:[\\da-f:\\.]*", - options: .caseInsensitive) - return regex.stringByReplacingMatches(in: sdp, options: [], range: NSRange(location: 0, length: sdp.utf16.count), withTemplate: "[ REDACTED_IPV6_ADDRESS ]") - } catch { - owsFailDebug("Could not redact IPv6 addresses.") - return "[Could not redact IPv6 addresses.]" - } - #endif - } -} - -protocol VideoCaptureSettingsDelegate: class { - var videoWidth: Int32 { get } - var videoHeight: Int32 { get } -} - -class VideoCaptureController { - - private let capturer: RTCCameraVideoCapturer - private weak var settingsDelegate: VideoCaptureSettingsDelegate? - private let serialQueue = DispatchQueue(label: "org.signal.videoCaptureController") - private var isUsingFrontCamera: Bool = true - - public var captureSession: AVCaptureSession { - return capturer.captureSession - } - - public init(capturer: RTCCameraVideoCapturer, settingsDelegate: VideoCaptureSettingsDelegate) { - self.capturer = capturer - self.settingsDelegate = settingsDelegate - } - - public func startCapture() { - serialQueue.sync { [weak self] in - guard let strongSelf = self else { - return - } - - strongSelf.startCaptureSync() - } - } - - public func stopCapture() { - serialQueue.sync { [weak self] in - guard let strongSelf = self else { - return - } - - strongSelf.capturer.stopCapture() - } - } - - public func switchCamera(isUsingFrontCamera: Bool) { - serialQueue.sync { [weak self] in - guard let strongSelf = self else { - return - } - - strongSelf.isUsingFrontCamera = isUsingFrontCamera - strongSelf.startCaptureSync() - } - } - - private func assertIsOnSerialQueue() { - if _isDebugAssertConfiguration(), #available(iOS 10.0, *) { - assertOnQueue(serialQueue) - } - } - - private func startCaptureSync() { - assertIsOnSerialQueue() - - let position: AVCaptureDevice.Position = isUsingFrontCamera ? .front : .back - guard let device: AVCaptureDevice = self.device(position: position) else { - owsFailDebug("unable to find captureDevice") - return - } - - guard let format: AVCaptureDevice.Format = self.format(device: device) else { - owsFailDebug("unable to find captureDevice") - return - } - - let fps = self.framesPerSecond(format: format) - capturer.startCapture(with: device, format: format, fps: fps) - } - - private func device(position: AVCaptureDevice.Position) -> AVCaptureDevice? { - let captureDevices = RTCCameraVideoCapturer.captureDevices() - guard let device = (captureDevices.first { $0.position == position }) else { - Logger.debug("unable to find desired position: \(position)") - return captureDevices.first - } - - return device - } - - private func format(device: AVCaptureDevice) -> AVCaptureDevice.Format? { - let formats = RTCCameraVideoCapturer.supportedFormats(for: device) - let targetWidth = settingsDelegate?.videoWidth ?? 0 - let targetHeight = settingsDelegate?.videoHeight ?? 0 - - var selectedFormat: AVCaptureDevice.Format? - var currentDiff: Int32 = Int32.max - - for format in formats { - let dimension = CMVideoFormatDescriptionGetDimensions(format.formatDescription) - let diff = abs(targetWidth - dimension.width) + abs(targetHeight - dimension.height) - if diff < currentDiff { - selectedFormat = format - currentDiff = diff - } - } - - if _isDebugAssertConfiguration(), let selectedFormat = selectedFormat { - let dimension = CMVideoFormatDescriptionGetDimensions(selectedFormat.formatDescription) - Logger.debug("selected format width: \(dimension.width) height: \(dimension.height)") - } - - assert(selectedFormat != nil) - - return selectedFormat - } - - private func framesPerSecond(format: AVCaptureDevice.Format) -> Int { - var maxFrameRate: Float64 = 0 - for range in format.videoSupportedFrameRateRanges { - maxFrameRate = max(maxFrameRate, range.maxFrameRate) - } - - return Int(maxFrameRate) - } -} - -// 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" - @unknown default: - owsFailDebug("Unexpected enum value.") - return "unknown" - } - } -} - -fileprivate extension RTCIceGatheringState { - var debugDescription: String { - switch self { - case .new: - return "new" - case .gathering: - return "gathering" - case .complete: - return "complete" - @unknown default: - owsFailDebug("Unexpected enum value.") - return "unknown" - } - } -} - -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" - @unknown default: - owsFailDebug("Unexpected enum value.") - return "unknown" - } - } -} - -fileprivate extension RTCPeerConnectionState { - var debugDescription: String { - switch self { - case .new: - return "new" - case .connecting: - return "connecting" - case .connected: - return "connected" - case .disconnected: - return "disconnected" - case .failed: - return "failed" - case .closed: - return "closed" - @unknown default: - owsFailDebug("Unexpected enum value.") - return "unknown" - } - } -} diff --git a/ThirdParty/WebRTC b/ThirdParty/WebRTC index 7b0f20fdba..3cd3793606 160000 --- a/ThirdParty/WebRTC +++ b/ThirdParty/WebRTC @@ -1 +1 @@ -Subproject commit 7b0f20fdba38d3416412a3d34402d822654cdfec +Subproject commit 3cd37936061c51d2fcbec4bf8d84d8574e7e8031 diff --git a/protobuf/Makefile b/protobuf/Makefile deleted file mode 100644 index 828a82e6b1..0000000000 --- a/protobuf/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -PROTOC=protoc \ - --proto_path='./' -WRAPPER_SCRIPT=../Scripts/ProtoWrappers.py \ - --proto-dir='./' --verbose - -all: webrtc_data_proto - -webrtc_data_proto: OWSWebRTCDataProtos.proto - $(PROTOC) --swift_out=../Signal/src/Generated \ - OWSWebRTCDataProtos.proto - $(WRAPPER_SCRIPT) --dst-dir=../Signal/src/Generated \ - --wrapper-prefix=WebRTCProto --proto-prefix=WebRTCProtos --proto-file=OWSWebRTCDataProtos.proto diff --git a/protobuf/OWSWebRtcDataProtos.proto b/protobuf/OWSWebRtcDataProtos.proto deleted file mode 100644 index 36664807f9..0000000000 --- a/protobuf/OWSWebRtcDataProtos.proto +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ - -// iOS - since we use a modern proto-compiler, we must specify -// the legacy proto format. -syntax = "proto2"; - -// iOS - package name determines class prefix -package WebRTCProtos; - -option java_package = "org.thoughtcrime.securesms.webrtc"; -option java_outer_classname = "WebRtcDataProtos"; - -message Connected { - // @required - optional uint64 id = 1; -} - -message Hangup { - // @required - optional uint64 id = 1; -} - -message VideoStreamingStatus { - // @required - optional uint64 id = 1; - optional bool enabled = 2; -} - -message Data { - optional Connected connected = 1; - optional Hangup hangup = 2; - optional VideoStreamingStatus videoStreamingStatus = 3; -} From 185e787fce55114c538d89857ccb8ad7c91ee086 Mon Sep 17 00:00:00 2001 From: Jim Gustafson Date: Mon, 16 Sep 2019 12:17:16 -0700 Subject: [PATCH 4/8] Fix some error check handling For unexpected conditions, invoke owsFailDebug to trip the debugger. CHeck that callConnection is nil before setting it. --- Signal/src/call/CallService.swift | 14 +++++++------- Signal/src/call/PeerConnectionClient.swift | 10 ++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 42a791de13..c405f21b5c 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -1079,7 +1079,7 @@ private class SignalCallData: NSObject { do { try peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) } catch { - Logger.error("peerConnectionClient.setLocalVideoEnabled failed \(error)") + owsFailDebug("peerConnectionClient.setLocalVideoEnabled failed \(error)") } } @@ -1214,7 +1214,7 @@ private class SignalCallData: NSObject { do { try peerConnectionClient.setLocalAudioEnabled(enabled: false) } catch { - Logger.error("peerConnectionClient.setLocalAudioEnabled failed") + owsFailDebug("peerConnectionClient.setLocalAudioEnabled failed") } return } @@ -1222,7 +1222,7 @@ private class SignalCallData: NSObject { do { try peerConnectionClient.setLocalAudioEnabled(enabled: false) } catch { - Logger.error("peerConnectionClient.setLocalAudioEnabled failed") + owsFailDebug("peerConnectionClient.setLocalAudioEnabled failed") } return } @@ -1230,7 +1230,7 @@ private class SignalCallData: NSObject { do { try peerConnectionClient.setLocalAudioEnabled(enabled: false) } catch { - Logger.error("peerConnectionClient.setLocalAudioEnabled failed") + owsFailDebug("peerConnectionClient.setLocalAudioEnabled failed") } return } @@ -1238,7 +1238,7 @@ private class SignalCallData: NSObject { do { try peerConnectionClient.setLocalAudioEnabled(enabled: true) } catch { - Logger.error("peerConnectionClient.setLocalAudioEnabled failed") + owsFailDebug("peerConnectionClient.setLocalAudioEnabled failed") } } @@ -1296,7 +1296,7 @@ private class SignalCallData: NSObject { do { try peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) } catch { - Logger.error("peerConnectionClient.setLocalVideoEnabled failed \(error)") + owsFailDebug("peerConnectionClient.setLocalVideoEnabled failed \(error)") } } } @@ -1835,7 +1835,7 @@ private class SignalCallData: NSObject { try peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack) try peerConnectionClient.sendLocalVideoStatus(enabled: shouldHaveLocalVideoTrack) } catch { - Logger.error("error: \(error)") + owsFailDebug("error: \(error)") } } diff --git a/Signal/src/call/PeerConnectionClient.swift b/Signal/src/call/PeerConnectionClient.swift index f654ff4ff6..d90b8a192f 100644 --- a/Signal/src/call/PeerConnectionClient.swift +++ b/Signal/src/call/PeerConnectionClient.swift @@ -254,6 +254,11 @@ class PeerConnectionClient: NSObject, CallConnectionDelegate { Logger.debug("sendOffer") + guard self.callConnection == nil else { + owsFailDebug("callConnection was unexpectedly already set") + throw CallError.fatalError(description: "callConnection was unexpectedly already set") + } + guard let callConnectionfactory = self.callConnectionfactory else { throw CallError.fatalError(description: "Missing factory") } @@ -269,6 +274,11 @@ class PeerConnectionClient: NSObject, CallConnectionDelegate { Logger.debug("receivedOffer") + guard self.callConnection == nil else { + owsFailDebug("callConnection was unexpectedly already set") + throw CallError.fatalError(description: "callConnection was unexpectedly already set") + } + guard let callConnectionfactory = self.callConnectionfactory else { throw CallError.fatalError(description: "Missing factory") } From 28609b318494732639af33e6d8a01d5520e8e6e2 Mon Sep 17 00:00:00 2001 From: Jim Gustafson Date: Thu, 26 Sep 2019 11:33:49 -0700 Subject: [PATCH 5/8] PeerConnectionClient removal and hangup hardening This commit removes the PeerConnectionClient which had become a thin wrapper between CallService and CallConnection (RingRTC). Now, the handling is moved mostly to CallConnection and CallService directly interfaces with it. Also adds some guards to handle quick call/hangup scenarios on the caller side. There is also some optimization, in that we try to avoid sending hangups to the peer if no offer has been sent. --- Signal.xcodeproj/project.pbxproj | 6 +- Signal/Signal-Info.plist | 2 +- Signal/src/call/CallService.swift | 405 +++++++------ Signal/src/call/PeerConnectionClient.swift | 628 --------------------- 4 files changed, 250 insertions(+), 791 deletions(-) delete mode 100644 Signal/src/call/PeerConnectionClient.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 2589dbf3ec..386ba36b20 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -408,7 +408,6 @@ 45847E871E4283C30080EAB3 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45847E861E4283C30080EAB3 /* Intents.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 4585C4681ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4585C4671ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift */; }; 458967111DC117CC00E9DD21 /* AccountManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 458967101DC117CC00E9DD21 /* AccountManagerTest.swift */; }; - 458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 458DE9D51DEE3FD00071BB03 /* PeerConnectionClient.swift */; }; 458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 458E38361D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m */; }; 458E383A1D6699FA0094BD24 /* OWSDeviceProvisioningURLParserTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 458E38391D6699FA0094BD24 /* OWSDeviceProvisioningURLParserTest.m */; }; 459311FC1D75C948008DD4F0 /* OWSDeviceTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 459311FB1D75C948008DD4F0 /* OWSDeviceTableViewCell.m */; }; @@ -1171,9 +1170,9 @@ 34F308A01ECB469700BB7697 /* OWSBezierPathView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBezierPathView.h; sourceTree = ""; }; 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBezierPathView.m; sourceTree = ""; }; 34FDB29121FF986600A01202 /* UIView+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+OWS.swift"; sourceTree = ""; }; + 38AD4FA92310B7E00038BA75 /* SignalRingRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SignalRingRTC.framework; path = ThirdParty/WebRTC/Build/SignalRingRTC.framework; sourceTree = ""; }; 399D8A7F461D7253DFFB91C5 /* Pods-SignalTests.testable release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.testable release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.testable release.xcconfig"; sourceTree = ""; }; 4224D4E5D7921F25823ECDCA /* Pods-SignalPerformanceTests.testable release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalPerformanceTests.testable release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalPerformanceTests/Pods-SignalPerformanceTests.testable release.xcconfig"; sourceTree = ""; }; - 38AD4FA92310B7E00038BA75 /* SignalRingRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SignalRingRTC.framework; path = ThirdParty/WebRTC/Build/SignalRingRTC.framework; sourceTree = ""; }; 435EAC2E5E22D3F087EB3192 /* Pods-SignalShareExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.app store release.xcconfig"; sourceTree = ""; }; 4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "classic-quiet.aifc"; sourceTree = ""; }; 4503F1BC20470A5B00CEE724 /* classic.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = classic.aifc; sourceTree = ""; }; @@ -1239,7 +1238,6 @@ 4585C4671ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafetyNumberConfirmationAlert.swift; sourceTree = ""; }; 4589670F1DC117CC00E9DD21 /* SignalTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SignalTests-Bridging-Header.h"; sourceTree = ""; }; 458967101DC117CC00E9DD21 /* AccountManagerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AccountManagerTest.swift; path = Models/AccountManagerTest.swift; sourceTree = ""; }; - 458DE9D51DEE3FD00071BB03 /* PeerConnectionClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerConnectionClient.swift; sourceTree = ""; }; 458E38351D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDeviceProvisioningURLParser.h; sourceTree = ""; }; 458E38361D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDeviceProvisioningURLParser.m; sourceTree = ""; }; 458E38391D6699FA0094BD24 /* OWSDeviceProvisioningURLParserTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDeviceProvisioningURLParserTest.m; path = Models/OWSDeviceProvisioningURLParserTest.m; sourceTree = ""; }; @@ -2623,7 +2621,6 @@ 45794E841E0061CF00066731 /* UserInterface */, 45464DB81DFA03D8001D3FD6 /* Signaling */, 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */, - 458DE9D51DEE3FD00071BB03 /* PeerConnectionClient.swift */, 4574A5D51DD6704700C6B692 /* CallService.swift */, 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */, 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */, @@ -4214,7 +4211,6 @@ 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */, 45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */, 34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */, - 458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */, 45DDA6242090CEB500DE97F8 /* ConversationHeaderView.swift in Sources */, 3488F9362191CC4000E524CC /* ConversationMediaView.swift in Sources */, 45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */, diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 73c449e575..6a8ceea08a 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -9,7 +9,7 @@ OSXVersion 10.14.6 WebRTCCommit - 7b0f20fdba38d3416412a3d34402d822654cdfec M77 + 3cd37936061c51d2fcbec4bf8d84d8574e7e8031 Add RingRTC artifacts for beta CFBundleDevelopmentRegion en diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index c405f21b5c..5d5905f353 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -13,8 +13,8 @@ import SignalMessaging * `CallService` is a global singleton that manages the state of RingRTC-backed Signal * Calls (as opposed to legacy "RedPhone Calls" and "WebRTC Calls"). * - * It serves as a connection between the `CallUIAdapter` and the `PeerConnectionClient`. - * The PeerConnectionClient itself is a thin wrapper around the RingRTC `CallConnection`. + * It serves as a connection between the `CallUIAdapter` and the `CallConnection` + * provided by RingRTC. * * ## Signaling * @@ -66,14 +66,14 @@ import SignalMessaging * Show incoming call UI... * * If callee answers Call send `connected` message by invoking - * the `acceptCall` PeerConnectionClient API. + * the `accept` CallConnection API. * * <--[DC.ConnectedMesage]-- * Received connected message via remoteConnected call event... * Show Call is connected. * * Hang up (this could equally be sent by the Callee) - * --[DC.Hangup]--> (via PeerConnectionClient API) + * --[DC.Hangup]--> (via CallConnection API) * --[SS.Hangup]--> */ @@ -116,9 +116,9 @@ private class SignalCallData: NSObject { public var backgroundTask: OWSBackgroundTask? - // Used to ensure any received ICE messages wait until the peer connection client is set up. - let peerConnectionClientPromise: Promise - let peerConnectionClientResolver: Resolver + // Used to ensure any received ICE messages wait until the callConnection is set up. + let callConnectionPromise: Promise + let callConnectionResolver: Resolver // Used to ensure CallOffer was sent before sending any ICE updates. let readyToSendIceUpdatesPromise: Promise @@ -148,11 +148,27 @@ private class SignalCallData: NSObject { } } - var peerConnectionClient: PeerConnectionClient? { + var callConnectionFactory: CallConnectionFactory? { didSet { AssertIsOnMainThread() - Logger.debug(".peerConnectionClient setter: \(oldValue != nil) -> \(peerConnectionClient != nil) \(String(describing: peerConnectionClient))") + Logger.debug(".callConnectionFactory setter: \(oldValue != nil) -> \(callConnectionFactory != nil) \(String(describing: callConnectionFactory))") + } + } + + var callConnection: CallConnection? { + didSet { + AssertIsOnMainThread() + + Logger.debug(".callConnection setter: \(oldValue != nil) -> \(callConnection != nil) \(String(describing: callConnection))") + } + } + + var shouldSendHangup = false { + didSet { + AssertIsOnMainThread() + + Logger.info("") } } @@ -160,14 +176,16 @@ private class SignalCallData: NSObject { self.call = call self.delegate = delegate - let (peerConnectionClientPromise, peerConnectionClientResolver) = Promise.pending() - self.peerConnectionClientPromise = peerConnectionClientPromise - self.peerConnectionClientResolver = peerConnectionClientResolver + let (callConnectionPromise, callConnectionResolver) = Promise.pending() + self.callConnectionPromise = callConnectionPromise + self.callConnectionResolver = callConnectionResolver let (readyToSendIceUpdatesPromise, readyToSendIceUpdatesResolver) = Promise.pending() self.readyToSendIceUpdatesPromise = readyToSendIceUpdatesPromise self.readyToSendIceUpdatesResolver = readyToSendIceUpdatesResolver + self.callConnectionFactory = CallConnectionFactory() + super.init() } @@ -184,17 +202,29 @@ private class SignalCallData: NSObject { self.call.removeAllObservers() - // In case we're still waiting on the peer connection setup somewhere, we need to reject it to avoid a memory leak. + // In case we're still waiting on the callConnection setup somewhere, we need to reject it to avoid a memory leak. // There is no harm in rejecting a previously fulfilled promise. - self.peerConnectionClientResolver.reject(CallError.obsoleteCall(description: "Terminating call")) + self.callConnectionResolver.reject(CallError.obsoleteCall(description: "Terminating call")) // In case we're still waiting on this promise somewhere, we need to reject it to avoid a memory leak. // There is no harm in rejecting a previously fulfilled promise. self.readyToSendIceUpdatesResolver.reject(CallError.obsoleteCall(description: "Terminating call")) - peerConnectionClient?.close() + DispatchQueue.global().async { + if let callConnection = self.callConnection { + Logger.debug("Calling callConnection.close()") + callConnection.close() + } - outgoingIceUpdateQueue.removeAll() + if let callConnectionFactory = self.callConnectionFactory { + Logger.debug("Calling callConnectionFactory.close()") + callConnectionFactory.close() + } + + self.outgoingIceUpdateQueue.removeAll() + + Logger.debug("done") + } } // MARK: - Dependencies @@ -271,7 +301,7 @@ private class SignalCallData: NSObject { } // This class' state should only be accessed on the main queue. -@objc public class CallService: NSObject, CallObserver, PeerConnectionClientDelegate, SignalCallDataDelegate { +@objc public class CallService: NSObject, CallObserver, CallConnectionDelegate, SignalCallDataDelegate { // MARK: - Properties @@ -287,6 +317,8 @@ private class SignalCallData: NSObject { // MARK: Ivars + fileprivate var deferredHangupList: [UInt64: SignalCallData] = [:] + fileprivate var callData: SignalCallData? { didSet { AssertIsOnMainThread() @@ -327,11 +359,19 @@ private class SignalCallData: NSObject { } } - var peerConnectionClient: PeerConnectionClient? { + var callConnectionFactory: CallConnectionFactory? { get { AssertIsOnMainThread() - return callData?.peerConnectionClient + return callData?.callConnectionFactory + } + } + + var callConnection: CallConnection? { + get { + AssertIsOnMainThread() + + return callData?.callConnection } } @@ -424,7 +464,7 @@ private class SignalCallData: NSObject { if self.call != nil { Logger.warn("ending current call in. Did user toggle callkit preference while in a call?") - self.terminateCall() + self.terminate(callData: self.callData) } self.callUIAdapter = CallUIAdapter(callService: self, contactsManager: self.contactsManager, notificationPresenter: self.notificationPresenter) } @@ -436,6 +476,7 @@ private class SignalCallData: NSObject { */ func handleOutgoingCall(_ call: SignalCall) -> Promise { AssertIsOnMainThread() + Logger.info("") let callId = call.signalingId BenchEventStart(title: "Outgoing Call Connection", eventId: "call-\(callId)") @@ -458,28 +499,33 @@ private class SignalCallData: NSObject { call.callRecord = callRecord let promise = getIceServers().done { iceServers in - Logger.debug("got ice servers:\(iceServers) for call: \(call.identifiersForLogs)") + guard !call.isTerminated else { + Logger.debug("terminated call") + return + } guard self.call == call else { throw CallError.obsoleteCall(description: "obsolete call") } - guard callData.peerConnectionClient == nil else { - let errorDescription = "peerConnectionClient was unexpectedly already set." + guard callData.callConnection == nil else { + let errorDescription = "callConnection was unexpectedly already set." Logger.error(errorDescription) throw CallError.assertionError(description: errorDescription) } + Logger.debug("got ice servers:\(iceServers) for call: \(call.identifiersForLogs)") + let useTurnOnly = Environment.shared.preferences.doCallsHideIPAddress() - // Create a PeerConnectionClient to handle the call media. - let peerConnectionClient = PeerConnectionClient(delegate: self) - Logger.debug("setting peerConnectionClient for call: \(call.identifiersForLogs)") + // Create a CallConnection to handle the call media. + let callConnection = try self.callConnectionFactory?.createCallConnection(delegate: self, iceServers: iceServers, callId: callId, isOutgoing: true, hideIp: useTurnOnly) + Logger.debug("setting callConnection for call: \(call.identifiersForLogs)") - callData.peerConnectionClient = peerConnectionClient - callData.peerConnectionClientResolver.fulfill(()) + callData.callConnection = callConnection + callData.callConnectionResolver.fulfill(()) - try peerConnectionClient.sendOffer(iceServers: iceServers, useTurnOnly: useTurnOnly, callId: callId) + try callConnection?.sendOffer() } promise.catch { error in @@ -493,6 +539,7 @@ private class SignalCallData: NSObject { func readyToSendIceUpdates(call: SignalCall) { AssertIsOnMainThread() + Logger.debug("") guard let callData = self.callData else { self.handleFailedCall(failedCall: call, error: CallError.obsoleteCall(description: "obsolete call")) @@ -511,7 +558,7 @@ private class SignalCallData: NSObject { */ public func handleReceivedAnswer(thread: TSContactThread, callId: UInt64, sessionDescription: String) { AssertIsOnMainThread() - Logger.info("received call answer for call: \(callId) thread: \(thread.contactAddress)") + Logger.info("for call: \(callId) thread: \(thread.contactAddress)") guard let call = self.call else { Logger.warn("ignoring obsolete call: \(callId)") @@ -523,13 +570,13 @@ private class SignalCallData: NSObject { return } - guard let peerConnectionClient = self.peerConnectionClient else { - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "peerConnectionClient was unexpectedly nil")) + guard let callConnection = self.callConnection else { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "callConnection was unexpectedly nil")) return } do { - try peerConnectionClient.receivedAnswer(sdp: sessionDescription) + try callConnection.receivedAnswer(sdp: sessionDescription) } catch { Logger.debug("receivedAnswer failed with \(error)") @@ -542,6 +589,7 @@ private class SignalCallData: NSObject { */ public func handleMissedCall(_ call: SignalCall) { AssertIsOnMainThread() + Logger.info("") if call.callRecord == nil { // MJK TODO remove this timestamp param @@ -625,7 +673,7 @@ private class SignalCallData: NSObject { call.callRecord?.updateCallType(.outgoingMissed) callUIAdapter.remoteBusy(call) - terminateCall() + terminate(callData: self.callData) } /** @@ -634,6 +682,7 @@ private class SignalCallData: NSObject { */ public func handleReceivedOffer(thread: TSContactThread, callId: UInt64, sessionDescription callerSessionDescription: String) { AssertIsOnMainThread() + Logger.info("") BenchEventStart(title: "Incoming Call Connection", eventId: "call-\(callId)") @@ -668,7 +717,7 @@ private class SignalCallData: NSObject { callRecord.anyInsert(transaction: transaction) } - terminateCall() + terminate(callData: nil) return } @@ -699,7 +748,7 @@ private class SignalCallData: NSObject { case .answering, .localRinging, .connected, .localFailure, .localHangup, .remoteHangup, .remoteBusy, .reconnecting: // If one user calls another while the other has a "vestigial" call with // that same user, fail the old call. - terminateCall() + terminate(callData: nil) } } @@ -711,6 +760,8 @@ private class SignalCallData: NSObject { let callData = SignalCallData(call: newCall, delegate: self) self.callData = callData + callData.shouldSendHangup = true + Logger.debug("Enable backgroundTask") let backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in AssertIsOnMainThread() @@ -743,7 +794,7 @@ private class SignalCallData: NSObject { throw CallError.obsoleteCall(description: "getIceServers() response for obsolete call") } - assert(self.peerConnectionClient == nil, "Unexpected PeerConnectionClient instance") + assert(self.callConnection == nil, "Unexpected CallConnection instance") // For contacts not stored in our system contacts, we assume they are an unknown caller, and we force // a TURN connection, so as not to reveal any connectivity information (IP/port) to the caller. @@ -751,14 +802,14 @@ private class SignalCallData: NSObject { let useTurnOnly = isUnknownCaller || Environment.shared.preferences.doCallsHideIPAddress() - // Create a PeerConnectionClient to handle the call media. - let peerConnectionClient = PeerConnectionClient(delegate: self) - Logger.debug("setting peerConnectionClient for call: \(newCall.identifiersForLogs)") + // Create a CallConnection to handle the call media. + let callConnection = try self.callConnectionFactory?.createCallConnection(delegate: self, iceServers: iceServers, callId: callId, isOutgoing: false, hideIp: useTurnOnly) + Logger.debug("setting callConnection for call: \(newCall.identifiersForLogs)") - callData.peerConnectionClient = peerConnectionClient - callData.peerConnectionClientResolver.fulfill(()) + callData.callConnection = callConnection + callData.callConnectionResolver.fulfill(()) - try peerConnectionClient.receivedOffer(iceServers: iceServers, useTurnOnly: useTurnOnly, callId: callId, sdp: callerSessionDescription) + try callConnection?.receivedOffer(sdp: callerSessionDescription) }.recover { error in Logger.error("incoming call \(newCall.identifiersForLogs) failed with error: \(error)") @@ -776,17 +827,16 @@ private class SignalCallData: NSObject { */ public func handleRemoteAddedIceCandidate(thread: TSContactThread, callId: UInt64, sdp: String, lineIndex: Int32, mid: String) { AssertIsOnMainThread() - Logger.verbose("forming callId: \(callId)") + Logger.debug("for callId: \(callId)") guard let callData = self.callData else { Logger.info("ignoring remote ice update, since there is no current call.") return } - callData.peerConnectionClientPromise.done { + callData.callConnectionPromise.done { AssertIsOnMainThread() - - Logger.verbose("running callId: \(callId)") + Logger.debug("handling callId: \(callId)") guard let call = self.call else { Logger.warn("ignoring remote ice update for thread: \(String(describing: thread.uniqueId)) since there is no current call. Call already ended?") @@ -803,12 +853,12 @@ private class SignalCallData: NSObject { return } - guard let peerConnectionClient = self.peerConnectionClient else { - Logger.warn("ignoring remote ice update for thread: \(String(describing: thread.uniqueId)) since there is no current peerConnectionClient. Call already ended?") + guard let callConnection = self.callConnection else { + Logger.warn("ignoring remote ice update for thread: \(String(describing: thread.uniqueId)) since there is no current callConnection. Call already ended?") return } - try peerConnectionClient.receivedIceCandidate(sdp: sdp, lineIndex: lineIndex, sdpMid: mid) + try callConnection.receivedIceCandidate(sdp: sdp, lineIndex: lineIndex, sdpMid: mid) }.catch { error in Logger.error("handleRemoteAddedIceCandidate failed with error: \(error)") @@ -834,7 +884,7 @@ private class SignalCallData: NSObject { } guard call.state != .idle else { - // This will only be called for the current peerConnectionClient, so + // This will only be called for the current callConnection, so // fail the current call. self.handleFailedCurrentCall(error: CallError.assertionError(description: "ignoring local ice candidate, since call is now idle.")) return @@ -871,7 +921,6 @@ private class SignalCallData: NSObject { */ callData.sendOrEnqueue(outgoingIceUpdate: iceUpdateProto) }.catch { error in - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleLocalAddedIceCandidate(), file: #file, function: #function, line: #line) Logger.error("waitUntilReadyToSendIceUpdates failed with error: \(error)") }.retainUntilComplete() } @@ -886,7 +935,7 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() guard let callData = self.callData else { - // This will only be called for the current peerConnectionClient, so + // This will only be called for the current callConnection, so // fail the current call. handleFailedCurrentCall(error: CallError.assertionError(description: "ignoring \(#function) since there is no current call.")) return @@ -923,7 +972,7 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() guard let call = self.call else { - // This will only be called for the current peerConnectionClient, so + // This will only be called for the current callConnection, so // fail the current call. handleFailedCurrentCall(error: CallError.assertionError(description: "ignoring \(#function) since there is no current call.")) return @@ -946,7 +995,7 @@ private class SignalCallData: NSObject { */ public func handleRemoteHangup(thread: TSContactThread, callId: UInt64) { AssertIsOnMainThread() - Logger.debug("") + Logger.info("") guard let call = self.call else { // This may happen if we hang up slightly before they hang up. @@ -981,7 +1030,7 @@ private class SignalCallData: NSObject { callUIAdapter.remoteDidHangupCall(call) // self.call is nil'd in `terminateCall`, so it's important we update it's state *before* calling `terminateCall` - terminateCall() + terminate(callData: self.callData) } /** @@ -1014,8 +1063,7 @@ private class SignalCallData: NSObject { */ public func handleAnswerCall(_ call: SignalCall) { AssertIsOnMainThread() - - Logger.debug("") + Logger.info("") guard let currentCallData = self.callData else { handleFailedCall(failedCall: call, error: CallError.assertionError(description: "callData unexpectedly nil")) @@ -1029,8 +1077,8 @@ private class SignalCallData: NSObject { return } - guard let peerConnectionClient = self.peerConnectionClient else { - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "missing peerConnection client")) + guard let callConnection = self.callConnection else { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "missing callConnection")) return } @@ -1044,7 +1092,7 @@ private class SignalCallData: NSObject { call.callRecord = callRecord do { - try peerConnectionClient.acceptCall() + try callConnection.accept() } catch { self.handleFailedCall(failedCall: call, error: error) return @@ -1061,8 +1109,8 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() Logger.info("") - guard let peerConnectionClient = callData.peerConnectionClient else { - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "peerConnectionClient unexpectedly nil")) + guard let callConnection = callData.callConnection else { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "callConnection unexpectedly nil")) return } @@ -1074,12 +1122,12 @@ private class SignalCallData: NSObject { callData.call.state = .connected // We don't risk transmitting any media until the remote client has admitted to being connected. - ensureAudioState(call: callData.call, peerConnectionClient: peerConnectionClient) + ensureAudioState(call: callData.call, callConnection: callConnection) do { - try peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) + try callConnection.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) } catch { - owsFailDebug("peerConnectionClient.setLocalVideoEnabled failed \(error)") + owsFailDebug("callConnection.setLocalVideoEnabled failed \(error)") } } @@ -1090,12 +1138,19 @@ private class SignalCallData: NSObject { */ func handleLocalHungupCall(_ call: SignalCall) { AssertIsOnMainThread() + Logger.info("") + + guard let callData = self.callData else { + owsFailDebug("no valid callData found, nothing to hangup") + return + } guard let currentCall = self.call else { - Logger.info("No current call. Other party hung up just before us.") + Logger.info("no current call, other party hung up just before us") + + // Ensure that the call is terminated and cleaned up. + terminate(callData: self.callData) - // terminating the call might be redundant, but it shouldn't hurt. - terminateCall() return } @@ -1104,7 +1159,7 @@ private class SignalCallData: NSObject { return } - Logger.info("\(call.identifiersForLogs).") + Logger.info("\(call.identifiersForLogs)") if let callRecord = call.callRecord { if callRecord.callType == .outgoingIncomplete { @@ -1123,42 +1178,67 @@ private class SignalCallData: NSObject { owsFailDebug("missing call record") } + let originalState = call.state call.state = .localHangup - if let peerConnectionClient = self.peerConnectionClient { + if let callConnection = self.callConnection { // Stop audio capture ASAP - ensureAudioState(call: call, peerConnectionClient: peerConnectionClient) + ensureAudioState(call: call, callConnection: callConnection) - // Hangup a call, should send 'hangup' message via data channel. Will - // also call onSendHangup which will send a 'hangup' message via - // signaling and actually terminate the call. - do { - try peerConnectionClient.hangup() - } catch { - self.handleFailedCall(failedCall: call, error: error) - return + switch originalState { + case .dialing, .remoteRinging, .localRinging, .connected, .reconnecting: + // Only in these states would we expect the CallConnection to + // be negotiated and need to do a formal hangup (again). + Logger.debug("hanging up via CallConnection") + + do { + // Hangup a call, should send 'hangup' message via data channel. Will + // also call shouldSendHangup which will send a 'hangup' message via + // signaling and actually terminate the call. + + // Add the call to the deferredHangupList + deferredHangupList[call.signalingId] = callData + self.callData = nil + + Logger.debug("deferredHangupList.count: \(deferredHangupList.count)") + + try callConnection.hangup() + } catch { + // In case of error, clear the item from the list. + deferredHangupList.removeValue(forKey: call.signalingId) + + owsFailDebug("\(error)") + terminate(callData: callData) + } + default: + Logger.debug("") + + terminate(callData: callData) } } else { - Logger.info("ending call before peer connection created. Device offline or quick hangup.") + Logger.info("ending call before callConnection created (device offline or quick hangup)") + + guard callData.shouldSendHangup else { + terminate(callData: callData) + return + } - // If the call hasn't started yet, we don't have a data channel to communicate the hang up. Use Signal Service Message. do { let hangupBuilder = SSKProtoCallMessageHangup.builder(id: call.signalingId) let callMessage = OWSOutgoingCallMessage(thread: call.thread, hangupMessage: try hangupBuilder.build()) let sendPromise = messageSender.sendMessage(.promise, callMessage.asPreparer) .done { Logger.debug("sent hangup call message to \(call.thread.contactAddress)") - - self.terminateCall() + }.ensure { + self.terminate(callData: callData) }.catch { error in - OWSProdInfo(OWSAnalyticsEvents.callServiceErrorHandleLocalHungupCall(), file: #file, function: #function, line: #line) - Logger.error("failed to send hangup call message to \(call.thread.contactAddress) with error: \(error)") - self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "failed to send hangup call message")) - } + owsFailDebug("failed to send hangup call message to \(call.thread.contactAddress) with error: \(error)") + } sendPromise.retainUntilComplete() } catch { - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build hangup proto")) + owsFailDebug("couldn't build hangup proto") + terminate(callData: callData) } } } @@ -1179,12 +1259,12 @@ private class SignalCallData: NSObject { call.isMuted = isMuted - guard let peerConnectionClient = self.peerConnectionClient else { - // The peer connection might not be created yet. + guard let callConnection = self.callConnection else { + // The callConnection might not be created yet. return } - ensureAudioState(call: call, peerConnectionClient: peerConnectionClient) + ensureAudioState(call: call, callConnection: callConnection) } /** @@ -1201,44 +1281,44 @@ private class SignalCallData: NSObject { call.isOnHold = isOnHold - guard let peerConnectionClient = self.peerConnectionClient else { - // The peer connection might not be created yet. + guard let callConnection = self.callConnection else { + // The callConnection might not be created yet. return } - ensureAudioState(call: call, peerConnectionClient: peerConnectionClient) + ensureAudioState(call: call, callConnection: callConnection) } - func ensureAudioState(call: SignalCall, peerConnectionClient: PeerConnectionClient) { + func ensureAudioState(call: SignalCall, callConnection: CallConnection) { guard call.state == .connected else { do { - try peerConnectionClient.setLocalAudioEnabled(enabled: false) + try callConnection.setLocalAudioEnabled(enabled: false) } catch { - owsFailDebug("peerConnectionClient.setLocalAudioEnabled failed") + owsFailDebug("callConnection.setLocalAudioEnabled failed") } return } guard !call.isMuted else { do { - try peerConnectionClient.setLocalAudioEnabled(enabled: false) + try callConnection.setLocalAudioEnabled(enabled: false) } catch { - owsFailDebug("peerConnectionClient.setLocalAudioEnabled failed") + owsFailDebug("callConnection.setLocalAudioEnabled failed") } return } guard !call.isOnHold else { do { - try peerConnectionClient.setLocalAudioEnabled(enabled: false) + try callConnection.setLocalAudioEnabled(enabled: false) } catch { - owsFailDebug("peerConnectionClient.setLocalAudioEnabled failed") + owsFailDebug("callConnection.setLocalAudioEnabled failed") } return } do { - try peerConnectionClient.setLocalAudioEnabled(enabled: true) + try callConnection.setLocalAudioEnabled(enabled: true) } catch { - owsFailDebug("peerConnectionClient.setLocalAudioEnabled failed") + owsFailDebug("callConnection.setLocalAudioEnabled failed") } } @@ -1287,16 +1367,16 @@ private class SignalCallData: NSObject { call.hasLocalVideo = hasLocalVideo - guard let peerConnectionClient = self.peerConnectionClient else { - // The peer connection might not be created yet. + guard let callConnection = self.callConnection else { + // The callConnection might not be created yet. return } if call.state == .connected { do { - try peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) + try callConnection.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) } catch { - owsFailDebug("peerConnectionClient.setLocalVideoEnabled failed \(error)") + owsFailDebug("callConnection.setLocalVideoEnabled failed \(error)") } } } @@ -1311,23 +1391,23 @@ private class SignalCallData: NSObject { func setCameraSource(call: SignalCall, isUsingFrontCamera: Bool) { AssertIsOnMainThread() - guard let peerConnectionClient = self.peerConnectionClient else { + guard let callConnection = self.callConnection else { return } do { - try peerConnectionClient.setCameraSource(isUsingFrontCamera: isUsingFrontCamera) + try callConnection.setCameraSource(isUsingFrontCamera: isUsingFrontCamera) } catch { - Logger.error("peerConnectionClient.setCameraSource failed") + owsFailDebug("callConnection.setCameraSource failed") } } - // MARK: - PeerConnectionClientDelegate + // MARK: - CallConnectionDelegate - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onCallEvent event: CallEvent, callId: UInt64) { + public func callConnection(_ callConnectionParam: CallConnection, onCallEvent event: CallEvent, callId: UInt64) { AssertIsOnMainThread() - guard peerConnectionClient == self.peerConnectionClient else { + guard callConnectionParam == self.callConnection else { Logger.debug("Ignoring event from obsolete client") return } @@ -1401,12 +1481,11 @@ private class SignalCallData: NSObject { } } - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onCallError error: String, callId: UInt64) { + public func callConnection(_ callConnectionParam: CallConnection, onCallError error: String, callId: UInt64) { AssertIsOnMainThread() - Logger.debug("Got an error from RingRTC: \(error)") - guard peerConnectionClient == self.peerConnectionClient else { + guard callConnectionParam == self.callConnection else { Logger.debug("Ignoring event from obsolete client") return } @@ -1446,10 +1525,10 @@ private class SignalCallData: NSObject { } } - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onAddRemoteVideoTrack track: RTCVideoTrack, callId: UInt64) { + public func callConnection(_ callConnectionParam: CallConnection, onAddRemoteVideoTrack track: RTCVideoTrack, callId: UInt64) { AssertIsOnMainThread() - guard peerConnectionClient == self.peerConnectionClient else { + guard callConnectionParam == self.callConnection else { Logger.debug("Ignoring event from obsolete client") return } @@ -1471,10 +1550,10 @@ private class SignalCallData: NSObject { fireDidUpdateVideoTracks() } - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onUpdateLocalVideoSession session: AVCaptureSession?, callId: UInt64) { + public func callConnection(_ callConnectionParam: CallConnection, onUpdateLocalVideoSession session: AVCaptureSession?, callId: UInt64) { AssertIsOnMainThread() - guard peerConnectionClient == self.peerConnectionClient else { + guard callConnectionParam == self.callConnection else { Logger.debug("Ignoring event from obsolete client") return } @@ -1496,12 +1575,11 @@ private class SignalCallData: NSObject { fireDidUpdateVideoTracks() } - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendOffer sdp: String, callId: UInt64) { + public func callConnection(_ callConnectionParam: CallConnection, shouldSendOffer sdp: String, callId: UInt64) { AssertIsOnMainThread() - Logger.debug("Got onSendOffer") - guard peerConnectionClient == self.peerConnectionClient else { + guard callConnectionParam == self.callConnection else { Logger.debug("Ignoring event from obsolete client") return } @@ -1513,12 +1591,19 @@ private class SignalCallData: NSObject { let call = callData.call + guard !call.isTerminated else { + Logger.debug("terminated call") + return + } + guard callId == call.signalingId else { owsFailDebug("should send offer for call with id:\(callId) but current call has id:\(call.signalingId)") handleFailedCurrentCall(error: CallError.assertionError(description: "should send offer for call with id:\(callId) but current call has id:\(call.signalingId)")) return } + callData.shouldSendHangup = true + do { let offerBuilder = SSKProtoCallMessageOffer.builder(id: call.signalingId, sessionDescription: sdp) let callMessage = OWSOutgoingCallMessage(thread: call.thread, offerMessage: try offerBuilder.build()) @@ -1526,6 +1611,11 @@ private class SignalCallData: NSObject { .done { Logger.debug("sent offer call message to \(call.thread.contactAddress)") + guard !call.isTerminated else { + Logger.debug("terminated call") + return + } + // Ultimately, RingRTC will start sending Ice Candidates at the proper // time, so we open the gate here right after sending the offer. self.readyToSendIceUpdates(call: call) @@ -1540,12 +1630,11 @@ private class SignalCallData: NSObject { } } - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendAnswer sdp: String, callId: UInt64) { + public func callConnection(_ callConnectionParam: CallConnection, shouldSendAnswer sdp: String, callId: UInt64) { AssertIsOnMainThread() - Logger.debug("Got onSendAnswer") - guard peerConnectionClient == self.peerConnectionClient else { + guard callConnectionParam == self.callConnection else { Logger.debug("Ignoring event from obsolete client") return } @@ -1570,6 +1659,11 @@ private class SignalCallData: NSObject { .done { Logger.debug("sent answer call message to \(call.thread.contactAddress)") + guard !call.isTerminated else { + Logger.debug("terminated call") + return + } + // Ultimately, RingRTC will start sending Ice Candidates at the proper // time, so we open the gate here right after sending the answer. self.readyToSendIceUpdates(call: call) @@ -1584,10 +1678,10 @@ private class SignalCallData: NSObject { } } - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendIceCandidates candidates: [RTCIceCandidate], callId: UInt64) { + public func callConnection(_ callConnectionParam: CallConnection, shouldSendIceCandidates candidates: [RTCIceCandidate], callId: UInt64) { AssertIsOnMainThread() - guard peerConnectionClient == self.peerConnectionClient else { + guard callConnectionParam == self.callConnection else { Logger.debug("Ignoring event from obsolete client") return } @@ -1613,45 +1707,40 @@ private class SignalCallData: NSObject { } } - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendHangup callId: UInt64) { + public func callConnection(_ callConnectionParam: CallConnection, shouldSendHangup callId: UInt64) { AssertIsOnMainThread() - Logger.debug("Got onSendHangup") - guard peerConnectionClient == self.peerConnectionClient else { - Logger.debug("Ignoring event from obsolete client") + guard let callData = deferredHangupList.removeValue(forKey: callId) else { + owsFailDebug("obsolete call: \(callId)") return } - guard let callData = self.callData else { - Logger.debug("Ignoring event from obsolete call") + Logger.debug("deferredHangupList.count: \(deferredHangupList.count)") + + guard callConnectionParam == callData.callConnection else { + Logger.debug("obsolete client: \(callId)") return } let call = callData.call - guard callId == call.signalingId else { - owsFailDebug("should send hangup for call with id:\(callId) but current call has id:\(call.signalingId)") - handleFailedCurrentCall(error: CallError.assertionError(description: "should send hangup for call with id:\(callId) but current call has id:\(call.signalingId)")) - return - } - do { let hangupBuilder = SSKProtoCallMessageHangup.builder(id: call.signalingId) let callMessage = OWSOutgoingCallMessage(thread: call.thread, hangupMessage: try hangupBuilder.build()) let sendPromise = messageSender.sendMessage(.promise, callMessage.asPreparer) .done { Logger.debug("sent hangup call message to \(call.thread.contactAddress)") - - self.terminateCall() + }.ensure { + self.terminate(callData: callData) }.catch { error in - Logger.error("failed to send hangup call message to \(call.thread.contactAddress) with error: \(error)") - self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "failed to send hangup call message")) + owsFailDebug("failed to send hangup call message to \(call.thread.contactAddress) with error: \(error)") } sendPromise.retainUntilComplete() } catch { - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build hangup proto")) + owsFailDebug("couldn't build hangup proto") + terminate(callData: callData) } } @@ -1745,25 +1834,27 @@ private class SignalCallData: NSObject { } // Only terminate the call if it is the current call. - terminateCall() + terminate(callData: self.callData) } /** * Clean up any existing call state and get ready to receive a new call. */ - private func terminateCall() { + private func terminate(callData: SignalCallData?) { AssertIsOnMainThread() - Logger.debug("") - let currentCallData = self.callData - self.callData = nil + callData?.terminate() - currentCallData?.terminate() + callUIAdapter.didTerminateCall(callData?.call) - self.callUIAdapter.didTerminateCall(currentCallData?.call) + if self.callData == callData { + // Terminating the current call. + fireDidUpdateVideoTracks() - fireDidUpdateVideoTracks() + // nil self.callData when terminating the current call. + self.callData = nil + } // Apparently WebRTC will sometimes disable device orientation notifications. // After every call ends, we need to ensure they are enabled. @@ -1823,7 +1914,7 @@ private class SignalCallData: NSObject { private func updateIsVideoEnabled() { AssertIsOnMainThread() - guard let peerConnectionClient = self.peerConnectionClient else { + guard let callConnection = self.callConnection else { return } @@ -1832,8 +1923,8 @@ private class SignalCallData: NSObject { Logger.info("shouldHaveLocalVideoTrack: \(shouldHaveLocalVideoTrack)") do { - try peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack) - try peerConnectionClient.sendLocalVideoStatus(enabled: shouldHaveLocalVideoTrack) + try callConnection.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack) + try callConnection.sendLocalVideoStatus(enabled: shouldHaveLocalVideoTrack) } catch { owsFailDebug("error: \(error)") } diff --git a/Signal/src/call/PeerConnectionClient.swift b/Signal/src/call/PeerConnectionClient.swift deleted file mode 100644 index d90b8a192f..0000000000 --- a/Signal/src/call/PeerConnectionClient.swift +++ /dev/null @@ -1,628 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -import Foundation -import PromiseKit -import SignalCoreKit -import SignalRingRTC -import WebRTC - -/** - * The PeerConnectionClient notifies it's delegate (the CallService) of key events in the call signaling life cycle - * - * The delegate's methods will always be called on the main thread. - */ -protocol PeerConnectionClientDelegate: class { - - /** - * Fired for various asynchronous RingRTC events. See CallConnection.CallEvent for more information. - */ - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onCallEvent event: CallEvent, callId: UInt64) - - /** - * Fired whenever RingRTC encounters an error. Should always be considered fatal and end the session. - */ - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onCallError error: String, callId: UInt64) - - /** - * Fired whenever the remote video track becomes active or inactive. - */ - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onAddRemoteVideoTrack track: RTCVideoTrack, callId: UInt64) - - /** - * Fired whenever the local video track becomes active or inactive. - */ - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, onUpdateLocalVideoSession session: AVCaptureSession?, callId: UInt64) - - /** - * Fired when an offer message should be sent over the signaling channel. - */ - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendOffer sdp: String, callId: UInt64) - - /** - * Fired when an answer message should be sent over the signaling channel. - */ - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendAnswer sdp: String, callId: UInt64) - - /** - * Fired when there are one or more local Ice Candidates to be sent over the signaling channel. - */ - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendIceCandidates candidates: [RTCIceCandidate], callId: UInt64) - - /** - * Fired when a hangup message should be sent over the signaling channel. - */ - func peerConnectionClient(_ peerConnectionClient: PeerConnectionClient, shouldSendHangup callId: UInt64) -} - -// In Swift (at least in Swift v3.3), weak variables aren't thread safe. It -// isn't safe to resolve/acquire/lock a weak reference into a strong reference -// while the instance might be being deallocated on another thread. -// -// PeerConnectionProxy provides thread-safe access to a strong reference. -// PeerConnectionClient has an PeerConnectionProxy to itself that its many async blocks -// (which run on more than one thread) can use to safely try to acquire a strong -// reference to the PeerConnectionClient. In ARC we'd normally, we'd avoid -// having an instance retain a strong reference to itself to avoid retain -// cycles, but it's safe in this case: PeerConnectionClient is owned (and only -// used by) a single entity CallService and CallService always calls -// [PeerConnectionClient terminate] when it is done with a PeerConnectionClient -// instance, so terminate is a reliable place where we can break the retain cycle. -// -// Note that we use the proxy in two ways: -// -// * As a delegate for the peer connection and the data channel, -// safely forwarding delegate method invocations to the PCC. -// * To safely obtain references to the PCC within the PCC's -// async blocks. -// -// This should be fixed in Swift 4, but it isn't. -// -// To test using the following scenarios: -// -// * Alice and Bob place simultaneous calls to each other. Both should get busy. -// Repeat 10-20x. Then verify that they can connect a call by having just one -// call the other. -// * Alice or Bob (randomly alternating) calls the other. Recipient (randomly) -// accepts call or hangs up. If accepted, Alice or Bob (randomly) hangs up. -// Repeat immediately, as fast as you can, 10-20x. -class PeerConnectionProxy: NSObject, CallConnectionDelegate { - private var value: PeerConnectionClient? - - deinit { - Logger.info("[PeerConnectionProxy] deinit") - } - - func set(value: PeerConnectionClient) { - objc_sync_enter(self) - self.value = value - objc_sync_exit(self) - } - - func get() -> PeerConnectionClient? { - objc_sync_enter(self) - let result = value - objc_sync_exit(self) - - if result == nil { - // Every time this method returns nil is a - // possible crash avoided. - Logger.verbose("cleared get.") - } - - return result - } - - func clear() { - Logger.info("") - - objc_sync_enter(self) - value = nil - objc_sync_exit(self) - } - - // MARK: - CallConnectionDelegate - - func callConnection(_ callConnection: CallConnection, onCallEvent event: CallEvent, callId: UInt64) { - self.get()?.callConnection(callConnection, onCallEvent: event, callId: callId) - } - - func callConnection(_ callConnection: CallConnection, onCallError error: String, callId: UInt64) { - self.get()?.callConnection(callConnection, onCallError: error, callId: callId) - } - - func callConnection(_ callConnection: CallConnection, onAddRemoteVideoTrack track: RTCVideoTrack, callId: UInt64) { - self.get()?.callConnection(callConnection, onAddRemoteVideoTrack: track, callId: callId) - } - - func callConnection(_ callConnection: CallConnection, onUpdateLocalVideoSession session: AVCaptureSession?, callId: UInt64) { - self.get()?.callConnection(callConnection, onUpdateLocalVideoSession: session, callId: callId) - } - - func callConnection(_ callConnection: CallConnection, shouldSendOffer sdp: String, callId: UInt64) { - self.get()?.callConnection(callConnection, shouldSendOffer: sdp, callId: callId) - } - - func callConnection(_ callConnection: CallConnection, shouldSendAnswer sdp: String, callId: UInt64) { - self.get()?.callConnection(callConnection, shouldSendAnswer: sdp, callId: callId) - } - - func callConnection(_ callConnection: CallConnection, shouldSendIceCandidates candidates: [RTCIceCandidate], callId: UInt64) { - self.get()?.callConnection(callConnection, shouldSendIceCandidates: candidates, callId: callId) - } - - func callConnection(_ callConnection: CallConnection, shouldSendHangup callId: UInt64) { - self.get()?.callConnection(callConnection, shouldSendHangup: callId) - } - - func callConnection(_ callConnection: CallConnection, shouldSendBusy callId: UInt64) { - self.get()?.callConnection(callConnection, shouldSendBusy: callId) - } -} - -/** - * `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 post-connected signaling (hangup, add video) - */ -class PeerConnectionClient: NSObject, CallConnectionDelegate { - - private static let signalingQueue = DispatchQueue(label: "CallServiceSignalingQueue") - - // Delegate is notified of key events in the call lifecycle. - // - // This property should only be accessed on the main thread. - private weak var delegate: PeerConnectionClientDelegate? - - // Connection - - private var callConnectionfactory: CallConnectionFactory? - private var callConnection: CallConnection? - - private let proxy = PeerConnectionProxy() - - // Note that we're deliberately leaking proxy instances using this - // collection to avoid EXC_BAD_ACCESS. Calls are rare and the proxy - // is tiny (a single property), so it's better to leak and be safe. - private static var expiredProxies = [PeerConnectionProxy]() - - init(delegate: PeerConnectionClientDelegate) { - AssertIsOnMainThread() - - self.delegate = delegate - - callConnectionfactory = CallConnectionFactory() - - super.init() - - proxy.set(value: self) - - Logger.debug("object! PeerConnectionClient created \(ObjectIdentifier(self))") - } - - deinit { - Logger.debug("object! PeerConnectionClient destroyed \(ObjectIdentifier(self))") - } - - public func close() { - AssertIsOnMainThread() - - Logger.debug("close") - - // Clear the delegate immediately so that we can guarantee that - // no delegate methods are called after terminate() returns. - delegate = nil - - // Clear the proxy immediately so that enqueued work is aborted - // going forward. - PeerConnectionClient.expiredProxies.append(proxy) - proxy.clear() - - // Don't use [weak self]; we always want to perform terminateInternal(). - PeerConnectionClient.signalingQueue.async { - // Some notes on preventing crashes while disposing of peerConnection for video calls - // from: https://groups.google.com/forum/#!searchin/discuss-webrtc/objc$20crash$20dealloc%7Csort:relevance/discuss-webrtc/7D-vk5yLjn8/rBW2D6EW4GYJ - // The sequence to make it work appears to be - // - // [capturer stop]; // I had to add this as a method to RTCVideoCapturer - // [localRenderer stop]; - // [remoteRenderer stop]; - // [peerConnection close]; - - if let callConnection = self.callConnection { - Logger.debug("Calling callConnection.close()") - callConnection.close() - } - self.callConnection = nil - Logger.debug("callConnection is nil") - - if let callConnectionfactory = self.callConnectionfactory { - Logger.debug("Calling callConnectionfactory.close()") - callConnectionfactory.close() - } - self.callConnectionfactory = nil - Logger.debug("callConnectionfactory is nil") - } - } - - // MARK: - Session negotiation - - public func sendOffer(iceServers: [RTCIceServer], useTurnOnly: Bool, callId: UInt64) throws { - AssertIsOnMainThread() - - Logger.debug("sendOffer") - - guard self.callConnection == nil else { - owsFailDebug("callConnection was unexpectedly already set") - throw CallError.fatalError(description: "callConnection was unexpectedly already set") - } - - guard let callConnectionfactory = self.callConnectionfactory else { - throw CallError.fatalError(description: "Missing factory") - } - - let callConnection = try callConnectionfactory.createCallConnection(delegate: proxy, iceServers: iceServers, callId: callId, isOutgoing: true, hideIp: useTurnOnly) - self.callConnection = callConnection - - try callConnection.sendOffer() - } - - public func receivedOffer(iceServers: [RTCIceServer], useTurnOnly: Bool, callId: UInt64, sdp: String) throws { - AssertIsOnMainThread() - - Logger.debug("receivedOffer") - - guard self.callConnection == nil else { - owsFailDebug("callConnection was unexpectedly already set") - throw CallError.fatalError(description: "callConnection was unexpectedly already set") - } - - guard let callConnectionfactory = self.callConnectionfactory else { - throw CallError.fatalError(description: "Missing factory") - } - - let callConnection = try callConnectionfactory.createCallConnection(delegate: proxy, iceServers: iceServers, callId: callId, isOutgoing: false, hideIp: useTurnOnly) - self.callConnection = callConnection - - try callConnection.receivedOffer(sdp: sdp) - } - - public func receivedAnswer(sdp: String) throws { - AssertIsOnMainThread() - - Logger.debug("receivedAnswer") - - guard let callConnection = self.callConnection else { - throw CallError.obsoleteCall(description: "Invalid callConnection") - } - - try callConnection.receivedAnswer(sdp: sdp) - } - - public func receivedIceCandidate(sdp: String, lineIndex: Int32, sdpMid: String) throws { - AssertIsOnMainThread() - - Logger.debug("receivedIceCandidate") - - guard let callConnection = self.callConnection else { - throw CallError.obsoleteCall(description: "Invalid callConnection") - } - - try callConnection.receivedIceCandidate(sdp: sdp, lineIndex: lineIndex, sdpMid: sdpMid) - } - - // MARK: - Session Control - - public func acceptCall() throws { - AssertIsOnMainThread() - - Logger.debug("acceptCall") - - guard let callConnection = self.callConnection else { - throw CallError.obsoleteCall(description: "Invalid callConnection") - } - - try callConnection.accept() - } - - public func hangup() throws { - AssertIsOnMainThread() - - Logger.debug("hangup") - - guard let callConnection = self.callConnection else { - throw CallError.obsoleteCall(description: "Invalid callConnection") - } - - try callConnection.hangup() - } - - public func setLocalAudioEnabled(enabled: Bool) throws { - AssertIsOnMainThread() - - Logger.debug("setLocalAudioEnabled \(enabled)") - - guard let callConnection = self.callConnection else { - throw CallError.obsoleteCall(description: "Invalid callConnection") - } - - try callConnection.setLocalAudioEnabled(enabled: enabled) - } - - public func setLocalVideoEnabled(enabled: Bool) throws { - AssertIsOnMainThread() - - Logger.debug("setLocalVideoEnabled \(enabled)") - - guard let callConnection = self.callConnection else { - throw CallError.obsoleteCall(description: "Invalid callConnection") - } - - try callConnection.setLocalVideoEnabled(enabled: enabled) - } - - public func sendLocalVideoStatus(enabled: Bool) throws { - AssertIsOnMainThread() - - Logger.debug("sendLocalVideoStatus \(enabled)") - - guard let callConnection = self.callConnection else { - throw CallError.obsoleteCall(description: "Invalid callConnection") - } - - try callConnection.sendLocalVideoStatus(enabled: enabled) - } - - public func setCameraSource(isUsingFrontCamera: Bool) throws { - AssertIsOnMainThread() - - Logger.debug("setCameraSource \(isUsingFrontCamera)") - - let proxyCopy = self.proxy - - PeerConnectionClient.signalingQueue.async { - guard let strongSelf = proxyCopy.get() else { return } - - guard let callConnection = strongSelf.callConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - do { - try callConnection.switchCamera(isUsingFrontCamera: isUsingFrontCamera) - } catch { - Logger.debug("callConnection.switchCamera failed with \(error)") - } - } - } - - // MARK: - CallConnectionDelegate - - internal func callConnection(_ callConnectionParam: CallConnection, onCallEvent event: CallEvent, callId: UInt64) { - Logger.debug("onCallEvent") - - let proxyCopy = self.proxy - - DispatchQueue.main.async { - Logger.debug("onCallEvent - main thread") - - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - - guard let callConnection = strongSelf.callConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - guard callConnection == callConnectionParam else { - owsFailDebug("Mismatched callConnection callback") - return - } - - strongDelegate.peerConnectionClient(strongSelf, onCallEvent: event, callId: callId) - } - } - - internal func callConnection(_ callConnectionParam: CallConnection, onCallError error: String, callId: UInt64) { - Logger.debug("onCallError") - - let proxyCopy = self.proxy - - DispatchQueue.main.async { - Logger.debug("onCallError - main thread") - - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - - guard let callConnection = strongSelf.callConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - guard callConnection == callConnectionParam else { - owsFailDebug("Mismatched callConnection callback") - return - } - - strongDelegate.peerConnectionClient(strongSelf, onCallError: error, callId: callId) - } - } - - internal func callConnection(_ callConnectionParam: CallConnection, onAddRemoteVideoTrack track: RTCVideoTrack, callId: UInt64) { - Logger.debug("onAddRemoteVideoTrack") - - let proxyCopy = self.proxy - - DispatchQueue.main.async { - Logger.debug("onAddRemoteVideoTrack - main thread") - - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - - guard let callConnection = strongSelf.callConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - guard callConnection == callConnectionParam else { - owsFailDebug("Mismatched callConnection callback") - return - } - - strongDelegate.peerConnectionClient(strongSelf, onAddRemoteVideoTrack: track, callId: callId) - } - } - - internal func callConnection(_ callConnectionParam: CallConnection, onUpdateLocalVideoSession session: AVCaptureSession?, callId: UInt64) { - Logger.debug("onUpdateLocalVideoSession") - - let proxyCopy = self.proxy - - DispatchQueue.main.async { - Logger.debug("onUpdateLocalVideoSession - main thread") - - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - - guard let callConnection = strongSelf.callConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - guard callConnection == callConnectionParam else { - owsFailDebug("Mismatched callConnection callback") - return - } - - strongDelegate.peerConnectionClient(strongSelf, onUpdateLocalVideoSession: session, callId: callId) - } - } - - internal func callConnection(_ callConnectionParam: CallConnection, shouldSendOffer sdp: String, callId: UInt64) { - Logger.debug("shouldSendOffer") - - let proxyCopy = self.proxy - - DispatchQueue.main.async { - Logger.debug("shouldSendOffer - main thread") - - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - - guard let callConnection = strongSelf.callConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - guard callConnection == callConnectionParam else { - owsFailDebug("Mismatched callConnection callback") - return - } - - strongDelegate.peerConnectionClient(strongSelf, shouldSendOffer: sdp, callId: callId) - } - } - - internal func callConnection(_ callConnectionParam: CallConnection, shouldSendAnswer sdp: String, callId: UInt64) { - Logger.debug("shouldSendAnswer") - - let proxyCopy = self.proxy - - DispatchQueue.main.async { - Logger.debug("shouldSendAnswer - main thread") - - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - - guard let callConnection = strongSelf.callConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - guard callConnection == callConnectionParam else { - owsFailDebug("Mismatched callConnection callback") - return - } - - strongDelegate.peerConnectionClient(strongSelf, shouldSendAnswer: sdp, callId: callId) - } - } - - internal func callConnection(_ callConnectionParam: CallConnection, shouldSendIceCandidates candidates: [RTCIceCandidate], callId: UInt64) { - Logger.debug("shouldSendIceCandidates") - - let proxyCopy = self.proxy - - DispatchQueue.main.async { - Logger.debug("shouldSendIceCandidates - main thread") - - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - - guard let callConnection = strongSelf.callConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - guard callConnection == callConnectionParam else { - owsFailDebug("Mismatched callConnection callback") - return - } - - strongDelegate.peerConnectionClient(strongSelf, shouldSendIceCandidates: candidates, callId: callId) - } - } - - internal func callConnection(_ callConnectionParam: CallConnection, shouldSendHangup callId: UInt64) { - Logger.debug("shouldSendHangup") - - let proxyCopy = self.proxy - - DispatchQueue.main.async { - Logger.debug("shouldSendHangup - main thread") - - guard let strongSelf = proxyCopy.get() else { return } - guard let strongDelegate = strongSelf.delegate else { return } - - guard let callConnection = strongSelf.callConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - - guard callConnection == callConnectionParam else { - owsFailDebug("Mismatched callConnection callback") - return - } - - strongDelegate.peerConnectionClient(strongSelf, shouldSendHangup: callId) - } - } - - internal func callConnection(_ callConnectionParam: CallConnection, shouldSendBusy callId: UInt64) { - // Not supported on iOS. CallService maintains the state necessary - // to send Busy messages when appropriate. - } - - // MARK: Test-only accessors - - internal func peerConnectionForTests() -> CallConnection { - AssertIsOnMainThread() - - var result: CallConnection? - PeerConnectionClient.signalingQueue.sync { - result = callConnection - Logger.info("") - } - return result! - } - - internal func flushSignalingQueueForTests() { - AssertIsOnMainThread() - - PeerConnectionClient.signalingQueue.sync { - // Noop. - } - } -} From 48dfd5e940570eb2713d0c4ec934ac7b71a51455 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 30 Sep 2019 12:47:16 -0300 Subject: [PATCH 6/8] Remove convenience accessors from call service. Improve coherence. --- Signal/src/AppDelegate.m | 8 +- Signal/src/call/CallService.swift | 404 +++++++++--------- Signal/src/call/NonCallKitCallUIAdaptee.swift | 12 +- .../call/UserInterface/CallUIAdapter.swift | 2 +- 4 files changed, 208 insertions(+), 218 deletions(-) diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index bd4046e16a..2248d28931 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -859,8 +859,8 @@ static NSTimeInterval launchStartedAt; // * It can be received if the user taps the "video" button for a contact in the // contacts app. If so, the correct response is to try to initiate a new call // to that user - unless there already is another call in progress. - if (AppEnvironment.shared.callService.call != nil) { - if ([address isEqualToAddress:AppEnvironment.shared.callService.call.remoteAddress]) { + if (AppEnvironment.shared.callService.currentCall != nil) { + if ([address isEqualToAddress:AppEnvironment.shared.callService.currentCall.remoteAddress]) { OWSLogWarn(@"trying to upgrade ongoing call to video."); [AppEnvironment.shared.callService handleCallKitStartVideo]; return; @@ -910,7 +910,7 @@ static NSTimeInterval launchStartedAt; return; } - if (AppEnvironment.shared.callService.call != nil) { + if (AppEnvironment.shared.callService.currentCall != nil) { OWSLogWarn(@"ignoring INStartAudioCallIntent due to ongoing WebRTC call."); return; } @@ -959,7 +959,7 @@ static NSTimeInterval launchStartedAt; return; } - if (AppEnvironment.shared.callService.call != nil) { + if (AppEnvironment.shared.callService.currentCall != nil) { OWSLogWarn(@"ignoring INStartCallIntent due to ongoing WebRTC call."); return; } diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 5d5905f353..4e223af1b0 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -300,6 +300,8 @@ private class SignalCallData: NSObject { } } +// MARK: - CallService + // This class' state should only be accessed on the main queue. @objc public class CallService: NSObject, CallObserver, CallConnectionDelegate, SignalCallDataDelegate { @@ -319,39 +321,50 @@ private class SignalCallData: NSObject { fileprivate var deferredHangupList: [UInt64: SignalCallData] = [:] + private var _callData: SignalCallData? fileprivate var callData: SignalCallData? { - didSet { + set { AssertIsOnMainThread() + let oldValue = _callData + _callData = newValue + oldValue?.delegate = nil oldValue?.call.removeObserver(self) - callData?.call.addObserverAndSyncState(observer: self) + newValue?.call.addObserverAndSyncState(observer: self) updateIsVideoEnabled() // Prevent device from sleeping while we have an active call. - if oldValue != callData { + if oldValue != newValue { if let oldValue = oldValue { DeviceSleepManager.sharedInstance.removeBlock(blockObject: oldValue) } - if let callData = callData { - DeviceSleepManager.sharedInstance.addBlock(blockObject: callData) + if let newValue = newValue { + DeviceSleepManager.sharedInstance.addBlock(blockObject: newValue) self.startCallTimer() } else { stopAnyCallTimer() } } - Logger.debug(".callData setter: \(oldValue?.call.identifiersForLogs as Optional) -> \(callData?.call.identifiersForLogs as Optional)") + Logger.debug(".callData setter: \(oldValue?.call.identifiersForLogs as Optional) -> \(newValue?.call.identifiersForLogs as Optional)") for observer in observers { - observer.value?.didUpdateCall(call: callData?.call) + observer.value?.didUpdateCall(call: newValue?.call) } } + get { + AssertIsOnMainThread() + + return _callData + } } + // NOTE: This accessor should only be used outside this class. + // Within this class use callData to ensure coherency. @objc - var call: SignalCall? { + var currentCall: SignalCall? { get { AssertIsOnMainThread() @@ -359,49 +372,6 @@ private class SignalCallData: NSObject { } } - var callConnectionFactory: CallConnectionFactory? { - get { - AssertIsOnMainThread() - - return callData?.callConnectionFactory - } - } - - var callConnection: CallConnection? { - get { - AssertIsOnMainThread() - - return callData?.callConnection - } - } - - var localCaptureSession: AVCaptureSession? { - get { - AssertIsOnMainThread() - - return callData?.localCaptureSession - } - } - - var remoteVideoTrack: RTCVideoTrack? { - get { - AssertIsOnMainThread() - - return callData?.remoteVideoTrack - } - } - - var isRemoteVideoEnabled: Bool { - get { - AssertIsOnMainThread() - - guard let callData = callData else { - return false - } - return callData.isRemoteVideoEnabled - } - } - @objc public override init() { super.init() @@ -462,10 +432,11 @@ private class SignalCallData: NSObject { @objc public func createCallUIAdapter() { AssertIsOnMainThread() - if self.call != nil { + if self.callData != nil { Logger.warn("ending current call in. Did user toggle callkit preference while in a call?") self.terminate(callData: self.callData) } + self.callUIAdapter = CallUIAdapter(callService: self, contactsManager: self.contactsManager, notificationPresenter: self.notificationPresenter) } @@ -481,7 +452,7 @@ private class SignalCallData: NSObject { let callId = call.signalingId BenchEventStart(title: "Outgoing Call Connection", eventId: "call-\(callId)") - guard self.call == nil else { + guard self.callData == nil else { let errorDescription = "call was unexpectedly already set." Logger.error(errorDescription) call.state = .localFailure @@ -504,7 +475,7 @@ private class SignalCallData: NSObject { return } - guard self.call == call else { + guard self.callData === callData else { throw CallError.obsoleteCall(description: "obsolete call") } @@ -514,12 +485,12 @@ private class SignalCallData: NSObject { throw CallError.assertionError(description: errorDescription) } - Logger.debug("got ice servers:\(iceServers) for call: \(call.identifiersForLogs)") + Logger.debug("got ice servers: \(iceServers) for call: \(call.identifiersForLogs)") let useTurnOnly = Environment.shared.preferences.doCallsHideIPAddress() // Create a CallConnection to handle the call media. - let callConnection = try self.callConnectionFactory?.createCallConnection(delegate: self, iceServers: iceServers, callId: callId, isOutgoing: true, hideIp: useTurnOnly) + let callConnection = try callData.callConnectionFactory?.createCallConnection(delegate: self, iceServers: iceServers, callId: callId, isOutgoing: true, hideIp: useTurnOnly) Logger.debug("setting callConnection for call: \(call.identifiersForLogs)") callData.callConnection = callConnection @@ -531,6 +502,11 @@ private class SignalCallData: NSObject { promise.catch { error in Logger.error("outgoing call \(call.identifiersForLogs) failed with error: \(error)") + guard self.callData === callData else { + Logger.debug("ignoring error for obsolete call") + return + } + self.handleFailedCall(failedCall: call, error: error) }.retainUntilComplete() @@ -545,7 +521,7 @@ private class SignalCallData: NSObject { self.handleFailedCall(failedCall: call, error: CallError.obsoleteCall(description: "obsolete call")) return } - guard callData.call == call else { + guard callData.call === call else { Logger.warn("ignoring \(#function) for call other than current call") return } @@ -560,17 +536,18 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() Logger.info("for call: \(callId) thread: \(thread.contactAddress)") - guard let call = self.call else { + guard let callData = self.callData else { Logger.warn("ignoring obsolete call: \(callId)") return } + let call = callData.call guard call.signalingId == callId else { Logger.warn("ignoring mismatched call: \(callId) currentCall: \(call.signalingId)") return } - guard let callConnection = self.callConnection else { + guard let callConnection = callData.callConnection else { handleFailedCall(failedCall: call, error: CallError.assertionError(description: "callConnection was unexpectedly nil")) return } @@ -653,10 +630,11 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() Logger.info("for thread: \(thread.contactAddress)") - guard let call = self.call else { + guard let callData = self.callData else { Logger.warn("ignoring obsolete call: \(callId)") return } + let call = callData.call guard call.signalingId == callId else { Logger.warn("ignoring mismatched call: \(callId) currentCall: \(call.signalingId)") @@ -722,8 +700,8 @@ private class SignalCallData: NSObject { return } - guard self.call == nil else { - let existingCall = self.call! + if let existingCallData = self.callData { + let existingCall = existingCallData.call // TODO on iOS10+ we can use CallKit to swap calls rather than just returning busy immediately. Logger.info("receivedCallOffer: \(newCall.identifiersForLogs) but we're already in call: \(existingCall.identifiersForLogs)") @@ -763,24 +741,20 @@ private class SignalCallData: NSObject { callData.shouldSendHangup = true Logger.debug("Enable backgroundTask") - let backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in + let backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)", completionBlock: { status in AssertIsOnMainThread() guard status == .expired else { return } - guard let strongSelf = self else { - return - } - let timeout = CallError.timeout(description: "background task time ran out before call connected.") - guard strongSelf.call == newCall else { + guard self.callData?.call === newCall else { Logger.warn("ignoring obsolete call") return } - strongSelf.handleFailedCall(failedCall: newCall, error: timeout) + self.handleFailedCall(failedCall: newCall, error: timeout) }) callData.backgroundTask = backgroundTask @@ -788,13 +762,13 @@ private class SignalCallData: NSObject { getIceServers().done { iceServers 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. - Logger.debug("got ice servers:\(iceServers) for call: \(newCall.identifiersForLogs)") + Logger.debug("got ice servers: \(iceServers) for call: \(newCall.identifiersForLogs)") - guard self.call == newCall else { + guard let currentCallData = self.callData, currentCallData.call === newCall else { throw CallError.obsoleteCall(description: "getIceServers() response for obsolete call") } - assert(self.callConnection == nil, "Unexpected CallConnection instance") + assert(currentCallData.callConnection == nil, "Unexpected CallConnection instance") // For contacts not stored in our system contacts, we assume they are an unknown caller, and we force // a TURN connection, so as not to reveal any connectivity information (IP/port) to the caller. @@ -803,7 +777,7 @@ private class SignalCallData: NSObject { let useTurnOnly = isUnknownCaller || Environment.shared.preferences.doCallsHideIPAddress() // Create a CallConnection to handle the call media. - let callConnection = try self.callConnectionFactory?.createCallConnection(delegate: self, iceServers: iceServers, callId: callId, isOutgoing: false, hideIp: useTurnOnly) + let callConnection = try currentCallData.callConnectionFactory?.createCallConnection(delegate: self, iceServers: iceServers, callId: callId, isOutgoing: false, hideIp: useTurnOnly) Logger.debug("setting callConnection for call: \(newCall.identifiersForLogs)") callData.callConnection = callConnection @@ -813,7 +787,7 @@ private class SignalCallData: NSObject { }.recover { error in Logger.error("incoming call \(newCall.identifiersForLogs) failed with error: \(error)") - guard self.call == newCall else { + guard self.callData?.call === newCall else { Logger.debug("ignoring error for obsolete call") return } @@ -829,8 +803,9 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() Logger.debug("for callId: \(callId)") - guard let callData = self.callData else { - Logger.info("ignoring remote ice update, since there is no current call.") + guard let callData = self.callData, + callData.call.signalingId == callId else { + Logger.warn("ignoring ICE candidate for obsolete call: \(callId)") return } @@ -838,22 +813,18 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() Logger.debug("handling callId: \(callId)") - guard let call = self.call else { - Logger.warn("ignoring remote ice update for thread: \(String(describing: thread.uniqueId)) since there is no current call. Call already ended?") - return - } - - guard call.signalingId == callId else { - Logger.warn("ignoring mismatched call: \(callId) currentCall: \(call.signalingId)") + guard callData === self.callData else { + Logger.warn("ignoring ICE candidate for obsolete call: \(callId)") return } + let call = callData.call guard thread.contactAddress == call.thread.contactAddress else { Logger.warn("ignoring remote ice update for thread: \(String(describing: thread.uniqueId)) due to thread mismatch. Call already ended?") return } - guard let callConnection = self.callConnection else { + guard let callConnection = callData.callConnection else { Logger.warn("ignoring remote ice update for thread: \(String(describing: thread.uniqueId)) since there is no current callConnection. Call already ended?") return } @@ -873,15 +844,19 @@ private class SignalCallData: NSObject { private func handleLocalAddedIceCandidate(_ iceCandidate: RTCIceCandidate, callData: SignalCallData) { AssertIsOnMainThread() - let call = callData.call + guard self.callData === callData else { + Logger.warn("Receive local ICE candidate for obsolete call.") + return + } // Wait until we've sent the CallOffer before sending any ice updates for the call to ensure // intuitive message ordering for other clients. callData.readyToSendIceUpdatesPromise.done { - guard call == self.call else { + guard callData === self.callData else { self.handleFailedCurrentCall(error: .obsoleteCall(description: "current call changed since we became ready to send ice updates")) return } + let call = callData.call guard call.state != .idle else { // This will only be called for the current callConnection, so @@ -971,12 +946,13 @@ private class SignalCallData: NSObject { private func handleReconnecting() { AssertIsOnMainThread() - guard let call = self.call else { + guard let callData = self.callData else { // This will only be called for the current callConnection, so // fail the current call. handleFailedCurrentCall(error: CallError.assertionError(description: "ignoring \(#function) since there is no current call.")) return } + let call = callData.call Logger.info("\(call.identifiersForLogs).") @@ -997,11 +973,12 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() Logger.info("") - guard let call = self.call else { + guard let callData = self.callData else { // This may happen if we hang up slightly before they hang up. handleFailedCurrentCall(error: .obsoleteCall(description:"call was unexpectedly nil")) return } + let call = callData.call guard call.signalingId == callId else { Logger.warn("ignoring mismatched call: \(callId) currentCall: \(call.signalingId)") @@ -1041,17 +1018,18 @@ private class SignalCallData: NSObject { @objc public func handleAnswerCall(localId: UUID) { AssertIsOnMainThread() - guard let call = self.call else { + guard let callData = self.callData else { // This should never happen; return to a known good state. owsFailDebug("call was unexpectedly nil") handleFailedCurrentCall(error: CallError.assertionError(description: "call was unexpectedly nil")) return } + let call = callData.call guard call.localId == localId else { // This should never happen; return to a known good state. - owsFailDebug("callLocalId:\(localId) doesn't match current calls: \(call.localId)") - handleFailedCurrentCall(error: CallError.assertionError(description: "callLocalId:\(localId) doesn't match current calls: \(call.localId)")) + owsFailDebug("callLocalId: \(localId) doesn't match current calls: \(call.localId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "callLocalId: \(localId) doesn't match current calls: \(call.localId)")) return } @@ -1065,19 +1043,19 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() Logger.info("") - guard let currentCallData = self.callData else { + guard let callData = self.callData else { handleFailedCall(failedCall: call, error: CallError.assertionError(description: "callData unexpectedly nil")) return } - guard call == currentCallData.call else { + guard call == callData.call else { // This could conceivably happen if the other party of an old call was slow to send us their answer // and we've subsequently engaged in another call. Don't kill the current call, but just ignore it. Logger.warn("ignoring \(#function) for call other than current call") return } - guard let callConnection = self.callConnection else { + guard let callConnection = callData.callConnection else { handleFailedCall(failedCall: call, error: CallError.assertionError(description: "missing callConnection")) return } @@ -1098,7 +1076,7 @@ private class SignalCallData: NSObject { return } - handleConnectedCall(currentCallData) + handleConnectedCall(callData) } /** @@ -1109,6 +1087,11 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() Logger.info("") + guard callData === self.callData else { + Logger.debug("Ignoring connected for obsolete call.") + return + } + let call = callData.call guard let callConnection = callData.callConnection else { handleFailedCall(failedCall: call, error: CallError.assertionError(description: "callConnection unexpectedly nil")) return @@ -1145,16 +1128,7 @@ private class SignalCallData: NSObject { return } - guard let currentCall = self.call else { - Logger.info("no current call, other party hung up just before us") - - // Ensure that the call is terminated and cleaned up. - terminate(callData: self.callData) - - return - } - - guard call == currentCall else { + guard callData.call === call else { handleFailedCall(failedCall: call, error: CallError.assertionError(description: "ignoring \(#function) for call other than current call")) return } @@ -1181,7 +1155,7 @@ private class SignalCallData: NSObject { let originalState = call.state call.state = .localHangup - if let callConnection = self.callConnection { + if let callConnection = callData.callConnection { // Stop audio capture ASAP ensureAudioState(call: call, callConnection: callConnection) @@ -1251,15 +1225,16 @@ private class SignalCallData: NSObject { func setIsMuted(call: SignalCall, isMuted: Bool) { AssertIsOnMainThread() - guard call == self.call else { + guard let callData = self.callData else { // This can happen after a call has ended. Reproducible on iOS11, when the other party ends the call. Logger.info("ignoring mute request for obsolete call") return } + let call = callData.call call.isMuted = isMuted - guard let callConnection = self.callConnection else { + guard let callConnection = callData.callConnection else { // The callConnection might not be created yet. return } @@ -1274,14 +1249,14 @@ private class SignalCallData: NSObject { func setIsOnHold(call: SignalCall, isOnHold: Bool) { AssertIsOnMainThread() - guard call == self.call else { - Logger.info("ignoring held request for obsolete call") + guard let callData = self.callData, call == callData.call else { + Logger.info("ignoring hold request for obsolete call") return } call.isOnHold = isOnHold - guard let callConnection = self.callConnection else { + guard let callConnection = callData.callConnection else { // The callConnection might not be created yet. return } @@ -1334,15 +1309,20 @@ private class SignalCallData: NSObject { owsFailDebug("could not identify frontmostViewController") return } + guard let callData = self.callData else { + owsFailDebug("Missing callData") + return + } - frontmostViewController.ows_askForCameraPermissions { [weak self] granted in - guard let strongSelf = self else { + frontmostViewController.ows_askForCameraPermissions { granted in + guard self.callData === callData else { + owsFailDebug("Ignoring camera permissions for obsolete call.") return } if granted { // Success callback; camera permissions are granted. - strongSelf.setHasLocalVideoWithCameraPermissions(hasLocalVideo: hasLocalVideo) + self.setHasLocalVideoWithCameraPermissions(hasLocalVideo: hasLocalVideo) } else { // Failed callback; camera permissions are _NOT_ granted. @@ -1358,16 +1338,17 @@ private class SignalCallData: NSObject { private func setHasLocalVideoWithCameraPermissions(hasLocalVideo: Bool) { AssertIsOnMainThread() - guard let call = self.call else { + guard let callData = self.callData else { // This can happen if you toggle local video right after // the other user ends the call. - Logger.debug("Ignoring event from obsolete call") + Logger.debug("Ignoring event from obsolete call.") return } + let call = callData.call call.hasLocalVideo = hasLocalVideo - guard let callConnection = self.callConnection else { + guard let callConnection = callData.callConnection else { // The callConnection might not be created yet. return } @@ -1391,7 +1372,8 @@ private class SignalCallData: NSObject { func setCameraSource(call: SignalCall, isUsingFrontCamera: Bool) { AssertIsOnMainThread() - guard let callConnection = self.callConnection else { + guard let callData = self.callData, + let callConnection = callData.callConnection else { return } @@ -1407,21 +1389,17 @@ private class SignalCallData: NSObject { public func callConnection(_ callConnectionParam: CallConnection, onCallEvent event: CallEvent, callId: UInt64) { AssertIsOnMainThread() - guard callConnectionParam == self.callConnection else { - Logger.debug("Ignoring event from obsolete client") - return - } - - guard let callData = self.callData else { - Logger.debug("Ignoring event from obsolete call") + guard let callData = self.callData, + callConnectionParam == callData.callConnection else { + Logger.debug("Ignoring event from obsolete call.") return } let call = callData.call guard callId == call.signalingId else { - owsFailDebug("received call event for call with id:\(callId) but current call has id:\(call.signalingId)") - handleFailedCurrentCall(error: CallError.assertionError(description: "received call event for call with id:\(callId) but current call has id:\(call.signalingId)")) + owsFailDebug("received call event for call with id: \(callId) but current call has id: \(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "received call event for call with id: \(callId) but current call has id: \(call.signalingId)")) return } @@ -1485,21 +1463,17 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() Logger.debug("Got an error from RingRTC: \(error)") - guard callConnectionParam == self.callConnection else { - Logger.debug("Ignoring event from obsolete client") - return - } - - guard let callData = self.callData else { - Logger.debug("Ignoring event from obsolete call") + guard let callData = self.callData, + callConnectionParam == callData.callConnection else { + Logger.debug("Ignoring event from obsolete call.") return } let call = callData.call guard callId == call.signalingId else { - owsFailDebug("received call error for call with id:\(callId) but current call has id:\(call.signalingId)") - handleFailedCurrentCall(error: CallError.assertionError(description: "received call error for call with id:\(callId) but current call has id:\(call.signalingId)")) + owsFailDebug("received call error for call with id: \(callId) but current call has id: \(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "received call error for call with id: \(callId) but current call has id: \(call.signalingId)")) return } @@ -1512,10 +1486,21 @@ private class SignalCallData: NSObject { .done { Logger.debug("sent hangup call message to \(call.thread.contactAddress)") + guard self.callData === callData else { + Logger.debug("Ignoring hangup send success for obsolete call.") + return + } + // We fail the call rather than terminate it. self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: error)) }.catch { error in Logger.error("failed to send hangup call message to \(call.thread.contactAddress) with error: \(error)") + + guard self.callData === callData else { + Logger.debug("Ignoring hangup send failure for obsolete call.") + return + } + self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "failed to send hangup call message")) } @@ -1528,21 +1513,17 @@ private class SignalCallData: NSObject { public func callConnection(_ callConnectionParam: CallConnection, onAddRemoteVideoTrack track: RTCVideoTrack, callId: UInt64) { AssertIsOnMainThread() - guard callConnectionParam == self.callConnection else { - Logger.debug("Ignoring event from obsolete client") - return - } - - guard let callData = self.callData else { - Logger.debug("Ignoring event from obsolete call") + guard let callData = self.callData, + callConnectionParam == callData.callConnection else { + Logger.debug("Ignoring event from obsolete call.") return } let call = callData.call guard callId == call.signalingId else { - owsFailDebug("received remote video track for call with id:\(callId) but current call has id:\(call.signalingId)") - handleFailedCurrentCall(error: CallError.assertionError(description: "received remote video track for call with id:\(callId) but current call has id:\(call.signalingId)")) + owsFailDebug("received remote video track for call with id: \(callId) but current call has id: \(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "received remote video track for call with id: \(callId) but current call has id: \(call.signalingId)")) return } @@ -1553,21 +1534,17 @@ private class SignalCallData: NSObject { public func callConnection(_ callConnectionParam: CallConnection, onUpdateLocalVideoSession session: AVCaptureSession?, callId: UInt64) { AssertIsOnMainThread() - guard callConnectionParam == self.callConnection else { - Logger.debug("Ignoring event from obsolete client") - return - } - - guard let callData = self.callData else { - Logger.debug("Ignoring event from obsolete call") + guard let callData = self.callData, + callConnectionParam == callData.callConnection else { + Logger.debug("Ignoring event from obsolete call.") return } let call = callData.call guard callId == call.signalingId else { - owsFailDebug("received local video session for call with id:\(callId) but current call has id:\(call.signalingId)") - handleFailedCurrentCall(error: CallError.assertionError(description: "received local video session for call with id:\(callId) but current call has id:\(call.signalingId)")) + owsFailDebug("received local video session for call with id: \(callId) but current call has id: \(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "received local video session for call with id: \(callId) but current call has id: \(call.signalingId)")) return } @@ -1579,13 +1556,9 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() Logger.debug("Got onSendOffer") - guard callConnectionParam == self.callConnection else { - Logger.debug("Ignoring event from obsolete client") - return - } - - guard let callData = self.callData else { - Logger.debug("Ignoring event from obsolete call") + guard let callData = self.callData, + callConnectionParam == callData.callConnection else { + Logger.debug("Ignoring event from obsolete call.") return } @@ -1597,8 +1570,8 @@ private class SignalCallData: NSObject { } guard callId == call.signalingId else { - owsFailDebug("should send offer for call with id:\(callId) but current call has id:\(call.signalingId)") - handleFailedCurrentCall(error: CallError.assertionError(description: "should send offer for call with id:\(callId) but current call has id:\(call.signalingId)")) + owsFailDebug("should send offer for call with id: \(callId) but current call has id: \(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "should send offer for call with id: \(callId) but current call has id: \(call.signalingId)")) return } @@ -1611,6 +1584,11 @@ private class SignalCallData: NSObject { .done { Logger.debug("sent offer call message to \(call.thread.contactAddress)") + guard self.callData === callData else { + Logger.debug("Ignoring call offer send success for obsolete call.") + return + } + guard !call.isTerminated else { Logger.debug("terminated call") return @@ -1621,6 +1599,12 @@ private class SignalCallData: NSObject { self.readyToSendIceUpdates(call: call) }.catch { error in Logger.error("failed to send offer call message to \(call.thread.contactAddress) with error: \(error)") + + guard self.callData === callData else { + Logger.debug("Ignoring call offer send failure for obsolete call.") + return + } + self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "failed to send offer call message")) } @@ -1634,21 +1618,16 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() Logger.debug("Got onSendAnswer") - guard callConnectionParam == self.callConnection else { - Logger.debug("Ignoring event from obsolete client") - return - } - - guard let callData = self.callData else { - Logger.debug("Ignoring event from obsolete call") + guard let callData = self.callData, callConnectionParam == callData.callConnection else { + Logger.debug("Ignoring event from obsolete call.") return } let call = callData.call guard callId == call.signalingId else { - owsFailDebug("should send answer for call with id:\(callId) but current call has id:\(call.signalingId)") - handleFailedCurrentCall(error: CallError.assertionError(description: "should send answer for call with id:\(callId) but current call has id:\(call.signalingId)")) + owsFailDebug("should send answer for call with id: \(callId) but current call has id: \(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "should send answer for call with id: \(callId) but current call has id: \(call.signalingId)")) return } @@ -1659,6 +1638,11 @@ private class SignalCallData: NSObject { .done { Logger.debug("sent answer call message to \(call.thread.contactAddress)") + guard self.callData === callData else { + Logger.debug("Ignoring call answer send success for obsolete call.") + return + } + guard !call.isTerminated else { Logger.debug("terminated call") return @@ -1669,6 +1653,12 @@ private class SignalCallData: NSObject { self.readyToSendIceUpdates(call: call) }.catch { error in Logger.error("failed to send answer call message to \(call.thread.contactAddress) with error: \(error)") + + guard self.callData === callData else { + Logger.debug("Ignoring call answer send failure for obsolete call.") + return + } + self.handleFailedCall(failedCall: call, error: CallError.assertionError(description: "failed to send answer call message")) } @@ -1681,21 +1671,16 @@ private class SignalCallData: NSObject { public func callConnection(_ callConnectionParam: CallConnection, shouldSendIceCandidates candidates: [RTCIceCandidate], callId: UInt64) { AssertIsOnMainThread() - guard callConnectionParam == self.callConnection else { - Logger.debug("Ignoring event from obsolete client") - return - } - - guard let callData = self.callData else { - Logger.debug("Ignoring event from obsolete call") + guard let callData = self.callData, callConnectionParam == callData.callConnection else { + Logger.debug("Ignoring event from obsolete call.") return } let call = callData.call guard callId == call.signalingId else { - owsFailDebug("should send ice candidate for call with id:\(callId) but current call has id:\(call.signalingId)") - handleFailedCurrentCall(error: CallError.assertionError(description: "should send ice candidate for call with id:\(callId) but current call has id:\(call.signalingId)")) + owsFailDebug("should send ice candidate for call with id: \(callId) but current call has id: \(call.signalingId)") + handleFailedCurrentCall(error: CallError.assertionError(description: "should send ice candidate for call with id: \(callId) but current call has id: \(call.signalingId)")) return } @@ -1782,7 +1767,7 @@ private class SignalCallData: NSObject { Logger.debug("") // Return to a known good state by ending the current call, if any. - handleFailedCall(failedCall: self.call, error: error) + handleFailedCall(failedCall: self.callData?.call, error: error) } // This method should be called when a fatal error occurred for a call. @@ -1823,7 +1808,7 @@ private class SignalCallData: NSObject { self.callUIAdapter.failCall(failedCall, error: callError) // Only terminate the current call if the error pertains to the current call. - guard failedCall == self.call else { + guard failedCall === self.callData?.call else { Logger.debug("ignoring obsolete call: \(failedCall.identifiersForLogs).") return } @@ -1848,7 +1833,7 @@ private class SignalCallData: NSObject { callUIAdapter.didTerminateCall(callData?.call) - if self.callData == callData { + if self.callData === callData { // Terminating the current call. fireDidUpdateVideoTracks() @@ -1897,9 +1882,10 @@ private class SignalCallData: NSObject { private func shouldHaveLocalVideoTrack() -> Bool { AssertIsOnMainThread() - guard let call = self.call else { + guard let callData = self.callData else { return false } + let call = callData.call // The iOS simulator doesn't provide any sort of camera capture // support or emulation (http://goo.gl/rHAnC1) so don't bother @@ -1914,7 +1900,7 @@ private class SignalCallData: NSObject { private func updateIsVideoEnabled() { AssertIsOnMainThread() - guard let callConnection = self.callConnection else { + guard let callConnection = self.callData?.callConnection else { return } @@ -1939,10 +1925,7 @@ private class SignalCallData: NSObject { observers.append(Weak(value: observer)) // Synchronize observer with current call state - let remoteVideoTrack = self.isRemoteVideoEnabled ? self.remoteVideoTrack : nil - observer.didUpdateVideoTracks(call: self.call, - localCaptureSession: self.localCaptureSession, - remoteVideoTrack: remoteVideoTrack) + fireDidUpdateVideoTracks(forObserver: observer) } // The observer-related methods should be invoked on the main thread. @@ -1964,14 +1947,23 @@ private class SignalCallData: NSObject { private func fireDidUpdateVideoTracks() { AssertIsOnMainThread() - let remoteVideoTrack = self.isRemoteVideoEnabled ? self.remoteVideoTrack : nil - for observer in observers { - observer.value?.didUpdateVideoTracks(call: self.call, - localCaptureSession: self.localCaptureSession, - remoteVideoTrack: remoteVideoTrack) + for weakObserver in observers { + if let observer = weakObserver.value { + fireDidUpdateVideoTracks(forObserver: observer) + } } } + private func fireDidUpdateVideoTracks(forObserver observer: CallServiceObserver) { + AssertIsOnMainThread() + + let isRemoteVideoEnabled = callData?.isRemoteVideoEnabled ?? false + let remoteVideoTrack = isRemoteVideoEnabled ? callData?.remoteVideoTrack : nil + observer.didUpdateVideoTracks(call: callData?.call, + localCaptureSession: callData?.localCaptureSession, + remoteVideoTrack: remoteVideoTrack) + } + // MARK: CallViewController Timer var activeCallTimer: Timer? @@ -1981,27 +1973,25 @@ private class SignalCallData: NSObject { stopAnyCallTimer() assert(self.activeCallTimer == nil) - self.activeCallTimer = WeakTimer.scheduledTimer(timeInterval: 1, target: self, userInfo: nil, repeats: true) { [weak self] timer in - guard let strongSelf = self else { - return - } + guard let callData = self.callData else { + owsFailDebug("Missing callData.") + return + } - guard let call = strongSelf.call else { + self.activeCallTimer = WeakTimer.scheduledTimer(timeInterval: 1, target: self, userInfo: nil, repeats: true) { timer in + guard callData === self.callData else { owsFailDebug("call has since ended. Timer should have been invalidated.") timer.invalidate() return } + let call = callData.call - strongSelf.ensureCallScreenPresented(call: call) + self.ensureCallScreenPresented(call: call) } } func ensureCallScreenPresented(call: SignalCall) { - guard let currentCall = self.call else { - owsFailDebug("obsolete call: \(call.identifiersForLogs)") - return - } - guard currentCall == call else { + guard self.callData?.call === call else { owsFailDebug("obsolete call: \(call.identifiersForLogs)") return } @@ -2045,7 +2035,7 @@ private class SignalCallData: NSObject { func outgoingIceUpdateDidFail(call: SignalCall, error: Error) { AssertIsOnMainThread() - guard self.call == call else { + guard self.callData?.call === call else { Logger.warn("obsolete call") return } diff --git a/Signal/src/call/NonCallKitCallUIAdaptee.swift b/Signal/src/call/NonCallKitCallUIAdaptee.swift index f7e055e4fe..d4ae57a0eb 100644 --- a/Signal/src/call/NonCallKitCallUIAdaptee.swift +++ b/Signal/src/call/NonCallKitCallUIAdaptee.swift @@ -72,7 +72,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { func answerCall(localId: UUID) { AssertIsOnMainThread() - guard let call = self.callService.call else { + guard let call = self.callService.currentCall else { owsFailDebug("No current call.") return } @@ -88,7 +88,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { func answerCall(_ call: SignalCall) { AssertIsOnMainThread() - guard call.localId == self.callService.call?.localId else { + guard call.localId == self.callService.currentCall?.localId else { owsFailDebug("localId does not match current call") return } @@ -106,7 +106,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { func localHangupCall(localId: UUID) { AssertIsOnMainThread() - guard let call = self.callService.call else { + guard let call = self.callService.currentCall else { owsFailDebug("No current call.") return } @@ -124,7 +124,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { // If both parties hang up at the same moment, // call might already be nil. - guard self.callService.call == nil || call.localId == self.callService.call?.localId else { + guard self.callService.currentCall == nil || call.localId == self.callService.currentCall?.localId else { owsFailDebug("localId does not match current call") return } @@ -153,7 +153,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { func setIsMuted(call: SignalCall, isMuted: Bool) { AssertIsOnMainThread() - guard call.localId == self.callService.call?.localId else { + guard call.localId == self.callService.currentCall?.localId else { owsFailDebug("localId does not match current call") return } @@ -164,7 +164,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) { AssertIsOnMainThread() - guard call.localId == self.callService.call?.localId else { + guard call.localId == self.callService.currentCall?.localId else { owsFailDebug("localId does not match current call") return } diff --git a/Signal/src/call/UserInterface/CallUIAdapter.swift b/Signal/src/call/UserInterface/CallUIAdapter.swift index 527045030a..81d3bff15f 100644 --- a/Signal/src/call/UserInterface/CallUIAdapter.swift +++ b/Signal/src/call/UserInterface/CallUIAdapter.swift @@ -65,7 +65,7 @@ extension CallUIAdaptee { internal func startAndShowOutgoingCall(address: SignalServiceAddress, hasLocalVideo: Bool) { AssertIsOnMainThread() - guard self.callService.call == nil else { + guard self.callService.currentCall == nil else { owsFailDebug("unexpectedly found an existing call when trying to start outgoing call: \(address)") return } From cc37a11eea8eaf888d061cba2d42ec5fee22e6d5 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 2 Oct 2019 21:43:13 -0300 Subject: [PATCH 7/8] RingRTC v0.1.2 --- ThirdParty/WebRTC | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ThirdParty/WebRTC b/ThirdParty/WebRTC index 3cd3793606..22531e59a3 160000 --- a/ThirdParty/WebRTC +++ b/ThirdParty/WebRTC @@ -1 +1 @@ -Subproject commit 3cd37936061c51d2fcbec4bf8d84d8574e7e8031 +Subproject commit 22531e59a3ec75298c98947927c5d0a56cdfef2c From 13193664cb3030394d9643673948893bee7494df Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 7 Oct 2019 18:12:59 -0300 Subject: [PATCH 8/8] "Bump build to 2.44.0.13." --- Signal/Signal-Info.plist | 2 +- SignalShareExtension/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 6a8ceea08a..e1d00562fb 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -47,7 +47,7 @@ CFBundleVersion - 2.44.0.12 + 2.44.0.13 ITSAppUsesNonExemptEncryption LOGS_EMAIL diff --git a/SignalShareExtension/Info.plist b/SignalShareExtension/Info.plist index 1291cbc0c9..6c727f076b 100644 --- a/SignalShareExtension/Info.plist +++ b/SignalShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.44.0 CFBundleVersion - 2.44.0.12 + 2.44.0.13 ITSAppUsesNonExemptEncryption NSAppTransportSecurity