Audio/video call statistics

This commit is contained in:
Benoit Martins 2024-05-02 14:16:02 +02:00
parent 4949ca329a
commit 32dc6ea345
15 changed files with 294 additions and 6 deletions

View file

@ -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 = "<group>"; };
D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_linphone_default_values; sourceTree = "<group>"; };
D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_third_party_default_values; sourceTree = "<group>"; };
D78E06272BE3811D00CE3783 /* CallStatsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallStatsModel.swift; sourceTree = "<group>"; };
D79622332B1DFE600037EACD /* DialerBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialerBottomSheet.swift; sourceTree = "<group>"; };
D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = "<group>"; };
D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = "<group>"; };
@ -526,6 +528,7 @@
children = (
D720E6AC2BAD822000DDFD87 /* ParticipantModel.swift */,
D714035A2BE11E00004BD8CA /* CallMediaEncryptionModel.swift */,
D78E06272BE3811D00CE3783 /* CallStatsModel.swift */,
);
path = Model;
sourceTree = "<group>";
@ -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 */,

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#4e6074" viewBox="0 0 256 256"><path d="M168,72V200a8,8,0,0,1-16,0V72a8,8,0,0,1,16,0Zm32-48a8,8,0,0,0-8,8V200a8,8,0,0,0,16,0V32A8,8,0,0,0,200,24Zm-80,80a8,8,0,0,0-8,8v88a8,8,0,0,0,16,0V112A8,8,0,0,0,120,104ZM80,144a8,8,0,0,0-8,8v48a8,8,0,0,0,16,0V152A8,8,0,0,0,80,144ZM40,184a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-8A8,8,0,0,0,40,184Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M168,72V200a8,8,0,0,1-16,0V72a8,8,0,0,1,16,0Zm32-48a8,8,0,0,0-8,8V200a8,8,0,0,0,16,0V32A8,8,0,0,0,200,24Zm-80,80a8,8,0,0,0-8,8v88a8,8,0,0,0,16,0V112A8,8,0,0,0,120,104ZM80,144a8,8,0,0,0-8,8v48a8,8,0,0,0,16,0V152A8,8,0,0,0,80,144ZM40,184a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-8A8,8,0,0,0,40,184Z"></path></svg>

Before

Width:  |  Height:  |  Size: 411 B

After

Width:  |  Height:  |  Size: 411 B

View file

@ -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
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M168,72V200a8,8,0,0,1-16,0V72a8,8,0,0,1,16,0Zm-48,32a8,8,0,0,0-8,8v88a8,8,0,0,0,16,0V112A8,8,0,0,0,120,104ZM80,144a8,8,0,0,0-8,8v48a8,8,0,0,0,16,0V152A8,8,0,0,0,80,144ZM40,184a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-8A8,8,0,0,0,40,184Z"></path></svg>

After

Width:  |  Height:  |  Size: 351 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#4e6074" viewBox="0 0 256 256"><path d="M88,152v48a8,8,0,0,1-16,0V152a8,8,0,0,1,16,0ZM40,184a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-8A8,8,0,0,0,40,184Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M88,152v48a8,8,0,0,1-16,0V152a8,8,0,0,1,16,0ZM40,184a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-8A8,8,0,0,0,40,184Z"></path></svg>

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 228 B

View file

@ -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
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M128,112v88a8,8,0,0,1-16,0V112a8,8,0,0,1,16,0ZM80,144a8,8,0,0,0-8,8v48a8,8,0,0,0,16,0V152A8,8,0,0,0,80,144ZM40,184a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-8A8,8,0,0,0,40,184Z"></path></svg>

After

Width:  |  Height:  |  Size: 290 B

View file

@ -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
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M48,192v8a8,8,0,0,1-16,0v-8a8,8,0,0,1,16,0Z"></path></svg>

After

Width:  |  Height:  |  Size: 167 B

View file

@ -221,6 +221,9 @@
},
"Attended transfer" : {
},
"Audio" : {
},
"Audio seulement" : {
@ -806,6 +809,9 @@
},
"Validate the device" : {
},
"Vidéo" : {
},
"Video Call" : {

View file

@ -398,7 +398,6 @@ class TelecomManager: ObservableObject {
} else {
self.remoteConfVideo = false
}
} else {
self.remoteConfVideo = false

View file

@ -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)

View file

@ -53,8 +53,6 @@ class CallMediaEncryptionModel: ObservableObject {
mediaEncryptionTmp = "Media encryption: " + "ZRTP"
case .DTLS:
mediaEncryptionTmp = "Media encryption: " + "DTLS"
default:
mediaEncryptionTmp = "Media encryption: " + "None"
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
}
}
}
}

View file

@ -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<AnyCancellable?>()
@ -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