diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 92fbb98c5..2dae51c9e 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -95,6 +95,7 @@ D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */; }; D783C77C2B1089B200622CC2 /* assistant_linphone_default_values in Resources */ = {isa = PBXBuildFile; fileRef = D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */; }; D783C77D2B1089B200622CC2 /* assistant_third_party_default_values in Resources */ = {isa = PBXBuildFile; fileRef = D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */; }; + D78E06282BE3811D00CE3783 /* CallStatsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78E06272BE3811D00CE3783 /* CallStatsModel.swift */; }; D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79622332B1DFE600037EACD /* DialerBottomSheet.swift */; }; D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */; }; D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */; }; @@ -248,6 +249,7 @@ D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewModel.swift; sourceTree = ""; }; D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_linphone_default_values; sourceTree = ""; }; D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_third_party_default_values; sourceTree = ""; }; + D78E06272BE3811D00CE3783 /* CallStatsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallStatsModel.swift; sourceTree = ""; }; D79622332B1DFE600037EACD /* DialerBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialerBottomSheet.swift; sourceTree = ""; }; D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = ""; }; D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = ""; }; @@ -526,6 +528,7 @@ children = ( D720E6AC2BAD822000DDFD87 /* ParticipantModel.swift */, D714035A2BE11E00004BD8CA /* CallMediaEncryptionModel.swift */, + D78E06272BE3811D00CE3783 /* CallStatsModel.swift */, ); path = Model; sourceTree = ""; @@ -993,6 +996,7 @@ D726E4392B16440C0083C415 /* ContactAvatarModel.swift in Sources */, 6613A0B02BAEB7F4008923A4 /* MeetingsListFragment.swift in Sources */, D76005F62B0798B00054B79A /* IntExtension.swift in Sources */, + D78E06282BE3811D00CE3783 /* CallStatsModel.swift in Sources */, D7E6D0512AEBDBD500A57AAF /* ContactsListBottomSheet.swift in Sources */, D75759322B56D40900E7AC10 /* ZRTPPopup.swift in Sources */, D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */, diff --git a/Linphone/Assets.xcassets/cell-signal-full.imageset/cell-signal-full.svg b/Linphone/Assets.xcassets/cell-signal-full.imageset/cell-signal-full.svg index 8a04f8fed..2149b9e0d 100644 --- a/Linphone/Assets.xcassets/cell-signal-full.imageset/cell-signal-full.svg +++ b/Linphone/Assets.xcassets/cell-signal-full.imageset/cell-signal-full.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/cell-signal-high.imageset/Contents.json b/Linphone/Assets.xcassets/cell-signal-high.imageset/Contents.json new file mode 100644 index 000000000..daffc78c7 --- /dev/null +++ b/Linphone/Assets.xcassets/cell-signal-high.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "cell-signal-high.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/cell-signal-high.imageset/cell-signal-high.svg b/Linphone/Assets.xcassets/cell-signal-high.imageset/cell-signal-high.svg new file mode 100644 index 000000000..0db07907d --- /dev/null +++ b/Linphone/Assets.xcassets/cell-signal-high.imageset/cell-signal-high.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/cell-signal-low.imageset/cell-signal-low.svg b/Linphone/Assets.xcassets/cell-signal-low.imageset/cell-signal-low.svg index fac7f934c..dd093bcc8 100644 --- a/Linphone/Assets.xcassets/cell-signal-low.imageset/cell-signal-low.svg +++ b/Linphone/Assets.xcassets/cell-signal-low.imageset/cell-signal-low.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/cell-signal-medium.imageset/Contents.json b/Linphone/Assets.xcassets/cell-signal-medium.imageset/Contents.json new file mode 100644 index 000000000..88c54ffd4 --- /dev/null +++ b/Linphone/Assets.xcassets/cell-signal-medium.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "cell-signal-medium.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/cell-signal-medium.imageset/cell-signal-medium.svg b/Linphone/Assets.xcassets/cell-signal-medium.imageset/cell-signal-medium.svg new file mode 100644 index 000000000..2a986fce4 --- /dev/null +++ b/Linphone/Assets.xcassets/cell-signal-medium.imageset/cell-signal-medium.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/cell-signal-none.imageset/Contents.json b/Linphone/Assets.xcassets/cell-signal-none.imageset/Contents.json new file mode 100644 index 000000000..763d945f9 --- /dev/null +++ b/Linphone/Assets.xcassets/cell-signal-none.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "cell-signal-none.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/cell-signal-none.imageset/cell-signal-none.svg b/Linphone/Assets.xcassets/cell-signal-none.imageset/cell-signal-none.svg new file mode 100644 index 000000000..2b1d4ba4f --- /dev/null +++ b/Linphone/Assets.xcassets/cell-signal-none.imageset/cell-signal-none.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 4a74bed5c..b94e6b17d 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -221,6 +221,9 @@ }, "Attended transfer" : { + }, + "Audio" : { + }, "Audio seulement" : { @@ -806,6 +809,9 @@ }, "Validate the device" : { + }, + "Vidéo" : { + }, "Video Call" : { diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index 72ff6e84e..e6b01dcf9 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -398,7 +398,6 @@ class TelecomManager: ObservableObject { } else { self.remoteConfVideo = false } - } else { self.remoteConfVideo = false diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index a25d1dc49..59d0c54bf 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -42,6 +42,7 @@ struct CallView: View { @State var audioRouteSheet: Bool = false @State var changeLayoutSheet: Bool = false @State var mediaEncryptedSheet: Bool = false + @State var callStatisticsSheet: Bool = false @State var optionsAudioRoute: Int = 1 @State var optionsChangeLayout: Int = 2 @State var imageAudioRoute: String = "" @@ -71,6 +72,12 @@ struct CallView: View { mediaEncryptedSheetBottomSheet() .presentationDetents([.medium]) } + .sheet(isPresented: $callStatisticsSheet, onDismiss: { + callStatisticsSheet = false + }) { + callStatisticsSheetBottomSheet() + .presentationDetents(!callViewModel.callStatsModel.isVideoEnabled ? [.fraction(0.3)] : [.medium]) + } .sheet(isPresented: $audioRouteSheet, onDismiss: { audioRouteSheet = false }) { @@ -100,6 +107,12 @@ struct CallView: View { mediaEncryptedSheetBottomSheet() .presentationDetents([.medium]) } + .sheet(isPresented: $callStatisticsSheet, onDismiss: { + callStatisticsSheet = false + }) { + callStatisticsSheetBottomSheet() + .presentationDetents(!callViewModel.callStatsModel.isVideoEnabled ? [.fraction(0.3)] : [.medium]) + } .sheet(isPresented: $audioRouteSheet, onDismiss: { audioRouteSheet = false }) { @@ -127,6 +140,11 @@ struct CallView: View { } onDismiss: { mediaEncryptedSheet = false } + .halfSheet(showSheet: $callStatisticsSheet) { + callStatisticsSheetBottomSheet() + } onDismiss: { + callStatisticsSheet = false + } .halfSheet(showSheet: $audioRouteSheet) { audioRouteBottomSheet() } onDismiss: { @@ -263,6 +281,74 @@ struct CallView: View { .background(Color.gray600) } + @ViewBuilder + func callStatisticsSheetBottomSheet() -> some View { + VStack { + if idiom != .pad && (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Spacer() + HStack { + Spacer() + Button("Close") { + mediaEncryptedSheet = false + } + } + .padding(.trailing) + } else { + Capsule() + .fill(Color.grayMain2c300) + .frame(width: 75, height: 5) + .padding(15) + } + + Text("Audio") + .default_text_style_white_600(styleSize: 15) + .padding(.top, 10) + + Spacer() + + Text(callViewModel.callStatsModel.audioCodec) + .default_text_style_white(styleSize: 15) + + Spacer() + + Text(callViewModel.callStatsModel.audioBandwidth) + .default_text_style_white(styleSize: 15) + + Spacer() + + if callViewModel.callStatsModel.isVideoEnabled { + Text("Vidéo") + .default_text_style_white_600(styleSize: 15) + .padding(.top, 10) + + Spacer() + + Text(callViewModel.callStatsModel.videoCodec) + .default_text_style_white(styleSize: 15) + + Spacer() + + Text(callViewModel.callStatsModel.videoBandwidth) + .default_text_style_white(styleSize: 15) + + Spacer() + + Text(callViewModel.callStatsModel.videoResolution) + .default_text_style_white(styleSize: 15) + + Spacer() + + Text(callViewModel.callStatsModel.videoFps) + .default_text_style_white(styleSize: 15) + + Spacer() + } + } + .frame(maxWidth: .infinity) + .background(Color.gray600) + } @ViewBuilder func audioRouteBottomSheet() -> some View { VStack(spacing: 0) { @@ -533,8 +619,9 @@ struct CallView: View { Spacer() Button { + callStatisticsSheet = true } label: { - Image("cell-signal-full") + Image(callViewModel.qualityIcon) .renderingMode(.template) .resizable() .foregroundStyle(.white) diff --git a/Linphone/UI/Call/Model/CallMediaEncryptionModel.swift b/Linphone/UI/Call/Model/CallMediaEncryptionModel.swift index a4eaf15c0..86174516b 100644 --- a/Linphone/UI/Call/Model/CallMediaEncryptionModel.swift +++ b/Linphone/UI/Call/Model/CallMediaEncryptionModel.swift @@ -53,8 +53,6 @@ class CallMediaEncryptionModel: ObservableObject { mediaEncryptionTmp = "Media encryption: " + "ZRTP" case .DTLS: mediaEncryptionTmp = "Media encryption: " + "DTLS" - default: - mediaEncryptionTmp = "Media encryption: " + "None" } } diff --git a/Linphone/UI/Call/Model/CallStatsModel.swift b/Linphone/UI/Call/Model/CallStatsModel.swift new file mode 100644 index 000000000..3afc78bf7 --- /dev/null +++ b/Linphone/UI/Call/Model/CallStatsModel.swift @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import Foundation +import linphonesw + +class CallStatsModel: ObservableObject { + var coreContext = CoreContext.shared + + @Published var audioCodec = "" + @Published var audioBandwidth = "" + + @Published var isVideoEnabled = false + @Published var videoCodec = "" + @Published var videoBandwidth = "" + @Published var videoResolution = "" + @Published var videoFps = "" + + func update(call: Call, stats: CallStats) { + coreContext.doOnCoreQueue { core in + if call.params != nil { + self.isVideoEnabled = call.params!.videoEnabled && call.currentParams!.videoDirection != .Inactive + switch stats.type { + case .Audio: + if call.currentParams != nil { + let payloadType = call.currentParams!.usedAudioPayloadType + let clockRate = (payloadType?.clockRate != nil ? payloadType!.clockRate : 0) / 1000 + let codecLabel = "Codec: " + "\(payloadType != nil ? payloadType!.mimeType : "")/\(clockRate) kHz" + + let uploadBandwidth = Int(stats.uploadBandwidth.rounded()) + let downloadBandwidth = Int(stats.downloadBandwidth.rounded()) + let bandwidthLabel = "Bandwidth: " + "↑ \(uploadBandwidth) kbits/s ↓ \(downloadBandwidth) kbits/s" + + DispatchQueue.main.async { + self.audioCodec = codecLabel + self.audioBandwidth = bandwidthLabel + } + } + case .Video: + if call.currentParams != nil { + let payloadType = call.currentParams!.usedVideoPayloadType + let clockRate = (payloadType?.clockRate != nil ? payloadType!.clockRate : 0) / 1000 + let codecLabel = "Codec: " + "\(payloadType!.mimeType)/\(clockRate) kHz" + + let uploadBandwidth = Int(stats.uploadBandwidth.rounded()) + let downloadBandwidth = Int(stats.downloadBandwidth.rounded()) + let bandwidthLabel = "Bandwidth: " + "↑ \(uploadBandwidth) kbits/s ↓ \(downloadBandwidth) kbits/s" + + let sentResolution = call.currentParams!.sentVideoDefinition!.name + let receivedResolution = call.currentParams!.receivedVideoDefinition!.name + let resolutionLabel = "Resolution: " + "↑ \(sentResolution!) ↓ \(receivedResolution!)" + + let sentFps = Int(call.currentParams!.sentFramerate.rounded()) + let receivedFps = Int(call.currentParams!.receivedFramerate.rounded()) + let fpsLabel = "FPS: " + "↑ \(sentFps) ↓ \(receivedFps)" + + DispatchQueue.main.async { + self.videoCodec = codecLabel + self.videoBandwidth = bandwidthLabel + self.videoResolution = resolutionLabel + self.videoFps = fpsLabel + } + } + default: break + } + } + } + } +} diff --git a/Linphone/UI/Call/ViewModel/CallViewModel.swift b/Linphone/UI/Call/ViewModel/CallViewModel.swift index a8c33de8d..a1848c03e 100644 --- a/Linphone/UI/Call/ViewModel/CallViewModel.swift +++ b/Linphone/UI/Call/ViewModel/CallViewModel.swift @@ -54,6 +54,10 @@ class CallViewModel: ObservableObject { @Published var activeSpeakerName: String = "" @Published var myParticipantModel: ParticipantModel? @Published var callMediaEncryptionModel = CallMediaEncryptionModel() + @Published var callStatsModel = CallStatsModel() + + @Published var qualityValue: Float = 0.0 + @Published var qualityIcon = "cell-signal-full" private var mConferenceSuscriptions = Set() @@ -153,6 +157,9 @@ class CallViewModel: ObservableObject { if self.currentCall != nil { self.callMediaEncryptionModel.update(call: self.currentCall!) + if self.currentCall!.audioStats != nil { + self.callStatsModel.update(call: self.currentCall!, stats: self.currentCall!.audioStats!) + } } DispatchQueue.main.async { @@ -201,6 +208,14 @@ class CallViewModel: ObservableObject { self.callMediaEncryptionModel.update(call: self.currentCall!) } }) + + self.callSuscriptions.insert(self.currentCall!.publisher?.onStatsUpdated?.postOnMainQueue {(cbVal: (call: Call, stats: CallStats)) in + if self.currentCall != nil { + self.callStatsModel.update(call: self.currentCall!, stats: cbVal.stats) + } + }) + + self.updateCallQualityIcon() } } } @@ -575,6 +590,7 @@ class CallViewModel: ObservableObject { if core.callsNb == 0 { DispatchQueue.main.async { self.timer.upstream.connect().cancel() + self.currentCall = nil } } } @@ -966,5 +982,32 @@ class CallViewModel: ObservableObject { }) } } + + func updateCallQualityIcon() { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.coreContext.doOnCoreQueue { core in + if self.currentCall != nil { + let quality = self.currentCall!.currentQuality + let icon = switch floor(quality) { + case 4, 5: "cell-signal-full" + case 3: "cell-signal-high" + case 2: "cell-signal-medium" + case 1: "cell-signal-low" + default: "cell-signal-none" + } + + print("iconiconicon \(icon) \(self.currentCall!.currentQuality)") + DispatchQueue.main.async { + self.qualityValue = quality + self.qualityIcon = icon + } + + if core.callsNb > 0 { + self.updateCallQualityIcon() + } + } + } + } + } } // swiftlint:enable type_body_length