diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index efac2f0e4a..a37f2044c1 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -283,8 +283,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 */; }; @@ -337,6 +335,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 */; }; @@ -406,7 +405,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 */; }; @@ -1060,8 +1058,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 = ""; }; @@ -1147,6 +1143,7 @@ 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 = ""; }; 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 = ""; }; @@ -1214,7 +1211,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 = ""; }; @@ -1518,6 +1514,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 */, @@ -2171,15 +2168,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 = ( @@ -2568,7 +2556,6 @@ 76EB03C318170B33006006FC /* AppDelegate.m */, 76EB03FE18170B33006006FC /* call */, 76EB041118170B33006006FC /* environment */, - 34C4E2542118957600BEA353 /* Generated */, 45D231751DC7E8C50034FA89 /* Jobs */, 457F3AC01D14A0F700C51351 /* Models */, 76EB041D18170B33006006FC /* network */, @@ -2586,7 +2573,6 @@ 45794E841E0061CF00066731 /* UserInterface */, 45464DB81DFA03D8001D3FD6 /* Signaling */, 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */, - 458DE9D51DEE3FD00071BB03 /* PeerConnectionClient.swift */, 4574A5D51DD6704700C6B692 /* CallService.swift */, 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */, 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */, @@ -2843,6 +2829,7 @@ D221A08C169C9E5E00537ABF /* Frameworks */ = { isa = PBXGroup; children = ( + 38AD4FA92310B7E00038BA75 /* SignalRingRTC.framework */, 3496955F21A2FC8100DCFE74 /* CloudKit.framework */, 4C9CA25C217E676900607C63 /* ZXingObjC.framework */, 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */, @@ -3444,6 +3431,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 = ( @@ -3945,7 +3933,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 */, @@ -4120,7 +4107,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 */, @@ -4160,7 +4146,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/AppDelegate.m b/Signal/src/AppDelegate.m index 59c4a70dc9..c42092cab5 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/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/ViewControllers/ConversationView/Cells/AudioWaveformProgressView.swift b/Signal/src/ViewControllers/ConversationView/Cells/AudioWaveformProgressView.swift index fd19015f97..7fb957c7a0 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 diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 43ddc064b3..4e223af1b0 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`. + * It serves as a connection between the `CallUIAdapter` and the `CallConnection` + * provided by RingRTC. * * ## 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 `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]--> - * --[SS.Hangup]--> + * --[DC.Hangup]--> (via CallConnection 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,13 +114,11 @@ 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 - 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 @@ -147,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("") } } @@ -159,18 +176,16 @@ 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 + 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() } @@ -187,22 +202,29 @@ 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. + // 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.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")) + 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?.terminate() - Logger.debug("setting peerConnectionClient") + 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 @@ -278,8 +300,10 @@ private class SignalCallData: NSObject { } } +// MARK: - CallService + // 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 @@ -295,79 +319,58 @@ private class SignalCallData: NSObject { // MARK: Ivars + 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() return callData?.call } } - var peerConnectionClient: PeerConnectionClient? { - get { - AssertIsOnMainThread() - - return callData?.peerConnectionClient - } - } - - 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() { @@ -429,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.terminateCall() + self.terminate(callData: self.callData) } + self.callUIAdapter = CallUIAdapter(callService: self, contactsManager: self.contactsManager, notificationPresenter: self.notificationPresenter) } @@ -443,15 +447,15 @@ private class SignalCallData: NSObject { */ func handleOutgoingCall(_ call: SignalCall) -> Promise { AssertIsOnMainThread() + Logger.info("") 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 - OWSProdError(OWSAnalyticsEvents.callServiceCallAlreadySet(), file: #file, function: #function, line: #line) return Promise(error: CallError.assertionError(description: errorDescription)) } @@ -465,91 +469,45 @@ private class SignalCallData: NSObject { } call.callRecord = callRecord - let promise = getIceServers() - .then { iceServers -> Promise in - Logger.debug("got ice servers:\(iceServers) for call: \(call.identifiersForLogs)") + let promise = getIceServers().done { iceServers in + guard !call.isTerminated else { + Logger.debug("terminated call") + return + } - guard self.call == call else { + guard self.callData === callData 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) - OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionAlreadySet(), file: #file, function: #function, line: #line) throw CallError.assertionError(description: errorDescription) } + Logger.debug("got ice servers: \(iceServers) for call: \(call.identifiersForLogs)") + let useTurnOnly = Environment.shared.preferences.doCallsHideIPAddress() - let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self, callDirection: .outgoing, useTurnOnly: useTurnOnly) - Logger.debug("setting peerConnectionClient for call: \(call.identifiersForLogs)") - callData.peerConnectionClient = peerConnectionClient - callData.peerConnectionClientResolver.fulfill(()) + // Create a CallConnection to handle the call media. + let callConnection = try callData.callConnectionFactory?.createCallConnection(delegate: self, iceServers: iceServers, callId: callId, isOutgoing: true, hideIp: useTurnOnly) + Logger.debug("setting callConnection for call: \(call.identifiersForLogs)") - 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") - } + callData.callConnection = callConnection + callData.callConnectionResolver.fulfill(()) - 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 callConnection?.sendOffer() } 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) + guard self.callData === callData else { + Logger.debug("ignoring error for obsolete call") + return } + + self.handleFailedCall(failedCall: call, error: error) }.retainUntilComplete() return promise @@ -557,12 +515,13 @@ private class SignalCallData: NSObject { func readyToSendIceUpdates(call: SignalCall) { AssertIsOnMainThread() + Logger.debug("") 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 { + guard callData.call === call else { Logger.warn("ignoring \(#function) for call other than current call") return } @@ -575,39 +534,31 @@ 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 { + 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 peerConnectionClient = self.peerConnectionClient else { - OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionMissing(), file: #file, function: #function, line: #line) - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "peerConnectionClient was unexpectedly nil")) + guard let callConnection = callData.callConnection else { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "callConnection was unexpectedly nil")) return } - let sessionDescription = RTCSessionDescription(type: .answer, sdp: sessionDescription) + do { + try callConnection.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) + } } /** @@ -615,6 +566,7 @@ private class SignalCallData: NSObject { */ public func handleMissedCall(_ call: SignalCall) { AssertIsOnMainThread() + Logger.info("") if call.callRecord == nil { // MJK TODO remove this timestamp param @@ -624,7 +576,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 } @@ -678,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)") @@ -698,7 +651,7 @@ private class SignalCallData: NSObject { call.callRecord?.updateCallType(.outgoingMissed) callUIAdapter.remoteBusy(call) - terminateCall() + terminate(callData: self.callData) } /** @@ -707,6 +660,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)") @@ -741,13 +695,13 @@ private class SignalCallData: NSObject { callRecord.anyInsert(transaction: transaction) } - terminateCall() + terminate(callData: nil) 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)") @@ -772,7 +726,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) } } @@ -784,33 +738,37 @@ 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 + callData.shouldSendHangup = true + + Logger.debug("Enable backgroundTask") + 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) }) - 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. - guard self.call == newCall else { + Logger.debug("got ice servers: \(iceServers) for call: \(newCall.identifiersForLogs)") + + guard let currentCallData = self.callData, currentCallData.call === newCall else { throw CallError.obsoleteCall(description: "getIceServers() response for obsolete call") } - assert(self.peerConnectionClient == nil, "Unexpected PeerConnectionClient 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. @@ -818,73 +776,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) - callData.peerConnectionClient = peerConnectionClient - callData.peerConnectionClientResolver.fulfill(()) + // Create a CallConnection to handle the call media. + let callConnection = try currentCallData.callConnectionFactory?.createCallConnection(delegate: self, iceServers: iceServers, callId: callId, isOutgoing: false, hideIp: useTurnOnly) + Logger.debug("setting callConnection for call: \(newCall.identifiersForLogs)") - let offerSessionDescription = RTCSessionDescription(type: .offer, sdp: callerSessionDescription) - let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) + callData.callConnection = callConnection + callData.callConnectionResolver.fulfill(()) - // 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 callConnection?.receivedOffer(sdp: callerSessionDescription) }.recover { error in - guard self.call == newCall else { - Logger.debug("ignoring error: \(error) for obsolete call: \(newCall.identifiersForLogs).") + Logger.error("incoming call \(newCall.identifiersForLogs) failed with error: \(error)") + + guard self.callData?.call === newCall else { + 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,41 +801,39 @@ private class SignalCallData: NSObject { */ public func handleRemoteAddedIceCandidate(thread: TSContactThread, callId: UInt64, sdp: String, lineIndex: Int32, mid: String) { AssertIsOnMainThread() - Logger.verbose("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.") + guard let callData = self.callData, + callData.call.signalingId == callId else { + Logger.warn("ignoring ICE candidate for obsolete call: \(callId)") return } - callData.peerConnectionClientPromise.done { + callData.callConnectionPromise.done { 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 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 = 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 } - 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 callConnection.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,28 +841,26 @@ 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.")) + guard self.callData === callData else { + Logger.warn("Receive local ICE candidate for obsolete call.") return } - let call = callData.call // 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 peerConnectionClient, so + // This will only be called for the current callConnection, 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 } @@ -992,24 +896,22 @@ 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() } /** - * 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 + // This will only be called for the current callConnection, 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,16 +943,16 @@ 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 + guard let callData = self.callData else { + // This will only be called for the current callConnection, 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 } + let call = callData.call Logger.info("\(call.identifiersForLogs).") @@ -1069,13 +971,14 @@ private class SignalCallData: NSObject { */ public func handleRemoteHangup(thread: TSContactThread, callId: UInt64) { AssertIsOnMainThread() - Logger.debug("") + 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)") @@ -1099,11 +1002,12 @@ private class SignalCallData: NSObject { } call.state = .remoteHangup + // Notify UI 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) } /** @@ -1114,19 +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") - OWSProdError(OWSAnalyticsEvents.callServiceCallMissing(), file: #file, function: #function, line: #line) 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)") - OWSProdError(OWSAnalyticsEvents.callServiceCallIdMismatch(), file: #file, function: #function, line: #line) - 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 } @@ -1138,25 +1041,22 @@ private class SignalCallData: NSObject { */ public func handleAnswerCall(_ call: SignalCall) { AssertIsOnMainThread() + Logger.info("") - Logger.debug("") - - guard let currentCallData = self.callData else { - OWSProdError(OWSAnalyticsEvents.callServiceCallDataMissing(), file: #file, function: #function, line: #line) + 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 peerConnectionClient = self.peerConnectionClient else { - OWSProdError(OWSAnalyticsEvents.callServicePeerConnectionMissing(), file: #file, function: #function, line: #line) - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "missing peerConnection client")) + guard let callConnection = callData.callConnection else { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "missing callConnection")) return } @@ -1169,20 +1069,14 @@ 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 callConnection.accept() } 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) + handleConnectedCall(callData) } /** @@ -1193,22 +1087,31 @@ private class SignalCallData: NSObject { AssertIsOnMainThread() 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")) + 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 } 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()) + ensureAudioState(call: callData.call, callConnection: callConnection) + + do { + try callConnection.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) + } catch { + owsFailDebug("callConnection.setLocalVideoEnabled failed \(error)") + } } /** @@ -1218,22 +1121,19 @@ private class SignalCallData: NSObject { */ func handleLocalHungupCall(_ call: SignalCall) { AssertIsOnMainThread() + Logger.info("") - guard let currentCall = self.call else { - Logger.info("No current call. Other party hung up just before us.") - - // terminating the call might be redundant, but it shouldn't hurt. - terminateCall() + guard let callData = self.callData else { + owsFailDebug("no valid callData found, nothing to hangup") return } - guard call == currentCall else { - OWSProdError(OWSAnalyticsEvents.callServiceCallMismatch(), file: #file, function: #function, line: #line) + guard callData.call === call else { handleFailedCall(failedCall: call, error: CallError.assertionError(description: "ignoring \(#function) for call other than current call")) return } - Logger.info("\(call.identifiersForLogs).") + Logger.info("\(call.identifiersForLogs)") if let callRecord = call.callRecord { if callRecord.callType == .outgoingIncomplete { @@ -1252,50 +1152,68 @@ private class SignalCallData: NSObject { owsFailDebug("missing call record") } + let originalState = call.state call.state = .localHangup - // TODO something like this lifted from Signal-Android. - // this.accountManager.cancelInFlightRequests(); - // this.messageSender.cancelInFlightRequests(); - - if let peerConnectionClient = self.peerConnectionClient { + if let callConnection = callData.callConnection { // Stop audio capture ASAP - ensureAudioState(call: call, peerConnectionClient: peerConnectionClient) + ensureAudioState(call: call, callConnection: callConnection) - // If the call is connected, we can send the hangup via the data channel for faster hangup. + 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") - var messageData: Data - do { - let hangupBuilder = WebRTCProtoHangup.builder(id: call.signalingId) - let dataBuilder = WebRTCProtoData.builder() - dataBuilder.setHangup(try hangupBuilder.build()) - messageData = try dataBuilder.buildSerializedData() - } catch { - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build proto")) + 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 callConnection created (device offline or quick hangup)") + + guard callData.shouldSendHangup else { + terminate(callData: callData) return } - peerConnectionClient.sendDataChannelMessage(data: messageData, description: "hangup", isCritical: true) - } else { - Logger.info("ending call before peer connection created. Device offline or quick hangup.") - } + 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)") + }.ensure { + self.terminate(callData: callData) + }.catch { error in + owsFailDebug("failed to send hangup call message to \(call.thread.contactAddress) with error: \(error)") + } - // 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()) - - 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() - - terminateCall() - } catch { - handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build proto")) + sendPromise.retainUntilComplete() + } catch { + owsFailDebug("couldn't build hangup proto") + terminate(callData: callData) + } } } @@ -1307,20 +1225,21 @@ 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 peerConnectionClient = self.peerConnectionClient else { - // The peer connection might not be created yet. + guard let callConnection = callData.callConnection else { + // The callConnection might not be created yet. return } - ensureAudioState(call: call, peerConnectionClient: peerConnectionClient) + ensureAudioState(call: call, callConnection: callConnection) } /** @@ -1330,36 +1249,52 @@ 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 peerConnectionClient = self.peerConnectionClient else { - // The peer connection might not be created yet. + guard let callConnection = callData.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 { - peerConnectionClient.setAudioEnabled(enabled: false) + do { + try callConnection.setLocalAudioEnabled(enabled: false) + } catch { + owsFailDebug("callConnection.setLocalAudioEnabled failed") + } return } guard !call.isMuted else { - peerConnectionClient.setAudioEnabled(enabled: false) + do { + try callConnection.setLocalAudioEnabled(enabled: false) + } catch { + owsFailDebug("callConnection.setLocalAudioEnabled failed") + } return } guard !call.isOnHold else { - peerConnectionClient.setAudioEnabled(enabled: false) + do { + try callConnection.setLocalAudioEnabled(enabled: false) + } catch { + owsFailDebug("callConnection.setLocalAudioEnabled failed") + } return } - peerConnectionClient.setAudioEnabled(enabled: true) + do { + try callConnection.setLocalAudioEnabled(enabled: true) + } catch { + owsFailDebug("callConnection.setLocalAudioEnabled failed") + } } /** @@ -1374,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) { + if granted { // Success callback; camera permissions are granted. - strongSelf.setHasLocalVideoWithCameraPermissions(hasLocalVideo: hasLocalVideo) + self.setHasLocalVideoWithCameraPermissions(hasLocalVideo: hasLocalVideo) } else { // Failed callback; camera permissions are _NOT_ granted. @@ -1398,22 +1338,27 @@ 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 peerConnectionClient = self.peerConnectionClient else { - // The peer connection might not be created yet. + guard let callConnection = callData.callConnection else { + // The callConnection might not be created yet. return } if call.state == .connected { - peerConnectionClient.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) + do { + try callConnection.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack()) + } catch { + owsFailDebug("callConnection.setLocalVideoEnabled failed \(error)") + } } } @@ -1427,217 +1372,363 @@ private class SignalCallData: NSObject { func setCameraSource(call: SignalCall, isUsingFrontCamera: Bool) { AssertIsOnMainThread() - guard let peerConnectionClient = self.peerConnectionClient else { + guard let callData = self.callData, + let callConnection = callData.callConnection else { return } - peerConnectionClient.setCameraSource(isUsingFrontCamera: isUsingFrontCamera) + do { + try callConnection.setCameraSource(isUsingFrontCamera: isUsingFrontCamera) + } catch { + owsFailDebug("callConnection.setCameraSource failed") + } } - /** - * 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) { + // MARK: - CallConnectionDelegate + + public func callConnection(_ callConnectionParam: CallConnection, onCallEvent event: CallEvent, callId: UInt64) { 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.")) + guard let callData = self.callData, + callConnectionParam == callData.callConnection else { + Logger.debug("Ignoring event from obsolete call.") return } + 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).") + 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() } } - private func handleIncomingConnected(call: SignalCall, - callData: SignalCallData, - connected: WebRTCProtoConnected) { + public func callConnection(_ callConnectionParam: CallConnection, onCallError error: String, callId: UInt64) { + AssertIsOnMainThread() + Logger.debug("Got an error from RingRTC: \(error)") + + 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)")) + 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)") + + 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")) + } + + sendPromise.retainUntilComplete() + } catch { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build hangup proto")) + } + } + + public func callConnection(_ callConnectionParam: CallConnection, onAddRemoteVideoTrack track: RTCVideoTrack, callId: UInt64) { 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.") + guard let callData = self.callData, + callConnectionParam == callData.callConnection else { + Logger.debug("Ignoring event from obsolete call.") 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)")) + 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)")) 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) { - AssertIsOnMainThread() - - guard peerConnectionClient == self.peerConnectionClient else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") - return - } - - self.handleIceConnected() - } - - func peerConnectionClientIceDisconnected(_ peerConnectionClient: PeerConnectionClient) { - AssertIsOnMainThread() - - guard peerConnectionClient == self.peerConnectionClient else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") - return - } - - self.handleIceDisconnected() - } - - /** - * The connection failed to establish. The clients will not be able to communicate. - */ - internal func peerConnectionClientIceFailed(_ peerConnectionClient: PeerConnectionClient) { - AssertIsOnMainThread() - - guard peerConnectionClient == self.peerConnectionClient else { - Logger.debug("Ignoring event from obsolete peerConnectionClient") - 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") - return - } - - self.handleLocalAddedIceCandidate(iceCandidate) - } - - /** - * 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") - 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?) { + public func callConnection(_ callConnectionParam: CallConnection, 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") + guard let callData = self.callData, + callConnectionParam == callData.callConnection else { + Logger.debug("Ignoring event from obsolete call.") return } - callData.remoteVideoTrack = videoTrack + 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() } + public func callConnection(_ callConnectionParam: CallConnection, shouldSendOffer sdp: String, callId: UInt64) { + AssertIsOnMainThread() + Logger.debug("Got onSendOffer") + + guard let callData = self.callData, + callConnectionParam == callData.callConnection else { + Logger.debug("Ignoring event from obsolete call.") + return + } + + 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()) + let sendPromise = messageSender.sendMessage(.promise, callMessage.asPreparer) + .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 + } + + // 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)") + + 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")) + } + + sendPromise.retainUntilComplete() + } catch { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build offer proto")) + } + } + + public func callConnection(_ callConnectionParam: CallConnection, shouldSendAnswer sdp: String, callId: UInt64) { + AssertIsOnMainThread() + Logger.debug("Got onSendAnswer") + + 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)")) + 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)") + + 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 + } + + // 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)") + + 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")) + } + + sendPromise.retainUntilComplete() + } catch { + handleFailedCall(failedCall: call, error: CallError.assertionError(description: "couldn't build answer proto")) + } + } + + public func callConnection(_ callConnectionParam: CallConnection, shouldSendIceCandidates candidates: [RTCIceCandidate], callId: UInt64) { + AssertIsOnMainThread() + + 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)")) + 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) + } + } + + public func callConnection(_ callConnectionParam: CallConnection, shouldSendHangup callId: UInt64) { + AssertIsOnMainThread() + Logger.debug("Got onSendHangup") + + guard let callData = deferredHangupList.removeValue(forKey: callId) else { + owsFailDebug("obsolete call: \(callId)") + return + } + + Logger.debug("deferredHangupList.count: \(deferredHangupList.count)") + + guard callConnectionParam == callData.callConnection else { + Logger.debug("obsolete client: \(callId)") + return + } + + let call = callData.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)") + }.ensure { + self.terminate(callData: callData) + }.catch { error in + owsFailDebug("failed to send hangup call message to \(call.thread.contactAddress) with error: \(error)") + } + + sendPromise.retainUntilComplete() + } catch { + owsFailDebug("couldn't build hangup proto") + terminate(callData: callData) + } + } + // MARK: - /** @@ -1676,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. @@ -1684,9 +1775,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,12 +1803,12 @@ 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 { + guard failedCall === self.callData?.call else { Logger.debug("ignoring obsolete call: \(failedCall.identifiersForLogs).") return } @@ -1719,25 +1819,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. @@ -1780,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 @@ -1797,32 +1900,20 @@ private class SignalCallData: NSObject { private func updateIsVideoEnabled() { AssertIsOnMainThread() - guard let call = self.call else { - return - } - guard let peerConnectionClient = self.peerConnectionClient else { + guard let callConnection = self.callData?.callConnection 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 callConnection.setLocalVideoEnabled(enabled: shouldHaveLocalVideoTrack) + try callConnection.sendLocalVideoStatus(enabled: shouldHaveLocalVideoTrack) } catch { - Logger.error("couldn't build proto") - return + owsFailDebug("error: \(error)") } - - peerConnectionClient.sendDataChannelMessage(data: messageData, description: "videoStreamingStatus", isCritical: false) } // MARK: - Observers @@ -1834,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. @@ -1859,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? @@ -1876,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 } @@ -1922,7 +2017,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 @@ -1941,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/PeerConnectionClient.swift b/Signal/src/call/PeerConnectionClient.swift deleted file mode 100644 index 18319df21d..0000000000 --- a/Signal/src/call/PeerConnectionClient.swift +++ /dev/null @@ -1,1386 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -import Foundation -import PromiseKit -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 - * - * The delegate's methods will always be called on the main thread. - */ -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. - */ - func peerConnectionClientIceConnected(_ peerconnectionClient: PeerConnectionClient) - - /** - * The connection failed to establish. The clients will not be able to communicate. - */ - func peerConnectionClientIceFailed(_ peerconnectionClient: PeerConnectionClient) - - /** - * 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. - */ - func peerConnectionClientIceDisconnected(_ peerconnectionClient: PeerConnectionClient) - - /** - * During the Signaling process each client generates IceCandidates locally, which contain information about how to - * reach the local client via the internet. The delegate must shuttle these IceCandates to the other (remote) client - * out of band, as part of establishing a connection over WebRTC. - */ - func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, addedLocalIceCandidate iceCandidate: RTCIceCandidate) - - /** - * Once the peerconnection is established, we can receive messages via the data channel, and notify the delegate. - */ - func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, received dataChannelMessage: WebRTCProtoData) - - /** - * Fired whenever the local video track become active or inactive. - */ - func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, didUpdateLocalVideoCaptureSession captureSession: AVCaptureSession?) - - /** - * Fired whenever the remote video track become active or inactive. - */ - func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, didUpdateRemoteVideoTrack videoTrack: RTCVideoTrack?) -} - -// 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, RTCPeerConnectionDelegate, RTCDataChannelDelegate { - - 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: - RTCPeerConnectionDelegate - - public func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) { - self.get()?.peerConnection(peerConnection, didChange: stateChanged) - } - - public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) { - self.get()?.peerConnection(peerConnection, didAdd: stream) - } - - public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) { - self.get()?.peerConnection(peerConnection, didRemove: stream) - } - - public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { - self.get()?.peerConnectionShouldNegotiate(peerConnection) - } - - public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) { - self.get()?.peerConnection(peerConnection, didChange: newState) - } - - public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) { - self.get()?.peerConnection(peerConnection, didChange: newState) - } - - public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { - self.get()?.peerConnection(peerConnection, didGenerate: candidate) - } - - public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) { - self.get()?.peerConnection(peerConnection, didRemove: candidates) - } - - 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) - } -} - -/** - * `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, RTCPeerConnectionDelegate, RTCDataChannelDelegate, VideoCaptureSettingsDelegate { - - 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. - // - // This property should only be accessed on the main thread. - private weak var delegate: PeerConnectionClientDelegate? - - // 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 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) { - 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) - - 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() - } - } - - deinit { - // TODO: We can demote this log level to debug once we're confident that - // this class is always deallocated. - Logger.info("[PeerConnectionClient] deinit") - } - - // 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) { - 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("") - - // 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 { - self.terminateInternal() - } - } - - private func terminateInternal() { - assertOnSignalingQueue() - Logger.debug("") - - // 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) { - 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: \(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 { - 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) - } catch { - Logger.error("failed to parse dataProto") - return - } - - DispatchQueue.main.async { - completion(dataChannelMessage) - } - } - } - - /** The data channel's |bufferedAmount| changed. */ - internal func dataChannel(_ dataChannel: RTCDataChannel, didChangeBufferedAmount amount: UInt64) { - Logger.debug("didChangeBufferedAmount: \(amount)") - } - - // MARK: - RTCPeerConnectionDelegate - - /** 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() - 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 { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - guard peerConnection == peerConnectionParam else { - owsFailDebug("mismatched peerConnection 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) - } - } - } - - /** Called when a remote peer closes a stream. */ - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didRemove stream: RTCMediaStream) { - Logger.debug("didRemove Stream:\(stream)") - } - - /** 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 { - guard let strongSelf = proxyCopy.get() else { return } - guard let peerConnection = strongSelf.peerConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - guard peerConnection == peerConnectionParam else { - owsFailDebug("mismatched peerConnection 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)") - } - } - } - - /** Called any time the IceGatheringState changes. */ - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didChange newState: RTCIceGatheringState) { - Logger.info("didChange IceGatheringState:\(newState.debugDescription)") - } - - /** 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() - 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 { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - guard peerConnection == peerConnectionParam else { - owsFailDebug("mismatched peerConnection callback.") - return - } - Logger.info("adding local ICE candidate:\(candidate.sdp)") - DispatchQueue.main.async { - completion(candidate) - } - } - } - - /** Called when a group of local Ice candidates have been removed. */ - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) { - Logger.debug("didRemove IceCandidates:\(candidates)") - } - - /** 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 { - guard let strongSelf = proxyCopy.get() else { return } - guard let peerConnection = strongSelf.peerConnection else { - Logger.debug("Ignoring obsolete event in terminated client") - return - } - guard peerConnection == peerConnectionParam else { - owsFailDebug("mismatched peerConnection 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) - } - } - } - - internal func peerConnection(_ peerConnectionParam: RTCPeerConnection, didChange connectionState: RTCPeerConnectionState) { - Logger.info("didChange PeerConnectionState:\(connectionState.debugDescription)") - } - - // MARK: Helpers - - /** - * We synchronize access to state in this class using this queue. - */ - private func assertOnSignalingQueue() { - assertOnQueue(type(of: self).signalingQueue) - } - - // MARK: Test-only accessors - - internal func peerConnectionForTests() -> RTCPeerConnection { - AssertIsOnMainThread() - - var result: RTCPeerConnection? - PeerConnectionClient.signalingQueue.sync { - result = peerConnection - Logger.info("") - } - return result! - } - - internal func dataChannelForTests() -> RTCDataChannel { - AssertIsOnMainThread() - - var result: RTCDataChannel? - PeerConnectionClient.signalingQueue.sync { - result = dataChannel - Logger.info("") - } - return result! - } - - internal func flushSignalingQueueForTests() { - AssertIsOnMainThread() - - PeerConnectionClient.signalingQueue.sync { - // Noop. - } - } -} - -/** - * 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/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 } diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 12fd0a7781..503faa8a95 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -823,6 +823,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. diff --git a/ThirdParty/WebRTC b/ThirdParty/WebRTC index 7b0f20fdba..22531e59a3 160000 --- a/ThirdParty/WebRTC +++ b/ThirdParty/WebRTC @@ -1 +1 @@ -Subproject commit 7b0f20fdba38d3416412a3d34402d822654cdfec +Subproject commit 22531e59a3ec75298c98947927c5d0a56cdfef2c 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; -}