Init conference call view

This commit is contained in:
Benoit Martins 2024-03-18 17:12:46 +01:00
parent bf4e4042d3
commit ab3b883442
9 changed files with 713 additions and 330 deletions

View file

@ -44,6 +44,7 @@
D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71FCA7E2AE1397200D2E43E /* ContactsListViewModel.swift */; };
D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71FCA802AE14CFC00D2E43E /* ContactsListFragment.swift */; };
D71FCA832AE14D6E00D2E43E /* ContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71FCA822AE14D6E00D2E43E /* ContactFragment.swift */; };
D720E6AD2BAD822000DDFD87 /* ParticipantModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D720E6AC2BAD822000DDFD87 /* ParticipantModel.swift */; };
D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72250622ADE9615008FB426 /* HistoryViewModel.swift */; };
D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72250682ADFBF2D008FB426 /* SideMenu.swift */; };
D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723432F2ACEFEF8009AA24E /* QrCodeScannerFragment.swift */; };
@ -180,6 +181,7 @@
D71FCA7E2AE1397200D2E43E /* ContactsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsListViewModel.swift; sourceTree = "<group>"; };
D71FCA802AE14CFC00D2E43E /* ContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsListFragment.swift; sourceTree = "<group>"; };
D71FCA822AE14D6E00D2E43E /* ContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactFragment.swift; sourceTree = "<group>"; };
D720E6AC2BAD822000DDFD87 /* ParticipantModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantModel.swift; sourceTree = "<group>"; };
D72250622ADE9615008FB426 /* HistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewModel.swift; sourceTree = "<group>"; };
D72250682ADFBF2D008FB426 /* SideMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenu.swift; sourceTree = "<group>"; };
D723432F2ACEFEF8009AA24E /* QrCodeScannerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrCodeScannerFragment.swift; sourceTree = "<group>"; };
@ -444,6 +446,14 @@
path = Viewmodel;
sourceTree = "<group>";
};
D720E6AB2BAD81C800DDFD87 /* Model */ = {
isa = PBXGroup;
children = (
D720E6AC2BAD822000DDFD87 /* ParticipantModel.swift */,
);
path = Model;
sourceTree = "<group>";
};
D72250612ADE95E4008FB426 /* ViewModel */ = {
isa = PBXGroup;
children = (
@ -597,6 +607,7 @@
isa = PBXGroup;
children = (
D75759302B56D3CE00E7AC10 /* Fragments */,
D720E6AB2BAD81C800DDFD87 /* Model */,
D7B99E972B29B37F00BE7BF2 /* ViewModel */,
D7B5678D2B28888F00DE63EB /* CallView.swift */,
);
@ -866,6 +877,7 @@
D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */,
D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */,
D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */,
D720E6AD2BAD822000DDFD87 /* ParticipantModel.swift in Sources */,
D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */,
D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */,
D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */,

View file

@ -270,9 +270,11 @@ final class CoreContext: ObservableObject {
}
}
func updatePresence(core : Core, presence : ConsolidatedPresence) {
func updatePresence(core: Core, presence: ConsolidatedPresence) {
if core.config!.getBool(section: "app", key: "publish_presence", defaultValue: true) {
core.consolidatedPresence = presence
DispatchQueue.main.async {
core.consolidatedPresence = presence
}
}
}
@ -283,7 +285,7 @@ final class CoreContext: ObservableObject {
Log.info("App is in foreground, PUBLISHING presence as Online")
self.updatePresence(core: self.mCore, presence: ConsolidatedPresence.Online)
try? self.mCore.start()
//try? self.mCore.start()
}
}
@ -297,7 +299,10 @@ final class CoreContext: ObservableObject {
// Flexisip will handle the Busy status depending on other devices
self.updatePresence(core: self.mCore, presence: ConsolidatedPresence.Offline)
// self.mCore.iterate()
self.mCore.stop()
if self.mCore.currentCall == nil {
//self.mCore.stop()
}
}
}

View file

@ -244,6 +244,9 @@
},
"Contacts" : {
},
"Content" : {
},
"Continue" : {
@ -485,6 +488,12 @@
},
"Other actions" : {
},
"Partage d'écran" : {
},
"Participants" : {
},
"password" : {
"extractionState" : "manual",
@ -625,6 +634,9 @@
},
"This contact will be deleted definitively." : {
},
"Title" : {
},
"TLS" : {
@ -693,4 +705,4 @@
}
},
"version" : "1.0"
}
}

View file

@ -45,6 +45,7 @@ class TelecomManager: ObservableObject {
@Published var callStarted: Bool = false
@Published var outgoingCallStarted: Bool = false
@Published var remoteVideo: Bool = false
@Published var remoteConfVideo: Bool = false
@Published var isRecordingByRemote: Bool = false
@Published var isPausedByRemote: Bool = false
@Published var refreshCallViewModel: Bool = false
@ -374,7 +375,19 @@ class TelecomManager: ObservableObject {
DispatchQueue.main.async {
let oldRemoteVideo = self.remoteVideo
self.remoteVideo = (core.videoActivationPolicy?.automaticallyAccept ?? false) && (call.remoteParams?.videoEnabled ?? false)
//self.remoteVideo = (core.videoActivationPolicy?.automaticallyAccept ?? false) && (call.remoteParams?.videoEnabled ?? false)
if call.conference != nil {
if call.conference!.activeSpeakerParticipantDevice != nil {
let direction = call.conference?.activeSpeakerParticipantDevice!.getStreamCapability(streamType: StreamType.Video)
self.remoteConfVideo = direction == MediaDirection.SendRecv || direction == MediaDirection.SendOnly
} else {
self.remoteConfVideo = true
}
} else {
self.remoteVideo = call.currentParams!.videoEnabled && call.currentParams!.videoDirection != MediaDirection.Inactive
self.remoteConfVideo = false
}
if self.remoteVideo && self.remoteVideo != oldRemoteVideo {
do {

View file

@ -17,13 +17,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// swiftlint:disable type_body_length
// swiftlint:disable line_length
import SwiftUI
import CallKit
import AVFAudio
import linphonesw
// swiftlint:disable type_body_length
// swiftlint:disable line_length
struct CallView: View {
@ObservedObject private var coreContext = CoreContext.shared
@ -38,7 +38,6 @@ struct CallView: View {
let pub = NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
@State var audioRouteSheet: Bool = false
@State var hideButtonsSheet: Bool = false
@State var options: Int = 1
@State var imageAudioRoute: String = ""
@State var angleDegree = 0.0
@ -47,6 +46,7 @@ struct CallView: View {
@State var maxBottomSheetHeight: CGFloat = 0.5
@State private var pointingUp: CGFloat = 0.0
@State private var currentOffset: CGFloat = 0.0
@State private var viewIsDisplayed = false
@Binding var fullscreenVideo: Bool
@Binding var isShowCallsListFragment: Bool
@ -59,7 +59,6 @@ struct CallView: View {
innerView(geometry: geo)
.sheet(isPresented: $audioRouteSheet, onDismiss: {
audioRouteSheet = false
hideButtonsSheet = false
}) {
innerBottomSheet()
.presentationDetents([.fraction(0.3)])
@ -77,7 +76,6 @@ struct CallView: View {
innerView(geometry: geo)
.sheet(isPresented: $audioRouteSheet, onDismiss: {
audioRouteSheet = false
hideButtonsSheet = false
}) {
innerBottomSheet()
.presentationDetents([.fraction(0.3)])
@ -96,7 +94,6 @@ struct CallView: View {
innerBottomSheet()
} onDismiss: {
audioRouteSheet = false
hideButtonsSheet = false
}
.halfSheet(showSheet: $showingDialer) {
DialerBottomSheet(
@ -320,7 +317,18 @@ struct CallView: View {
.padding(.all, 10)
}
if telecomManager.remoteVideo {
if !callViewModel.isConference && telecomManager.remoteVideo {
Button {
callViewModel.switchCamera()
} label: {
Image("camera-rotate")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 30, height: 30)
.padding(.horizontal)
}
} else if callViewModel.isConference && callViewModel.videoDisplayed {
Button {
callViewModel.switchCamera()
} label: {
@ -360,232 +368,7 @@ struct CallView: View {
}
}
ZStack {
VStack {
Spacer()
ZStack {
if callViewModel.isRemoteDeviceTrusted {
Circle()
.fill(Color.blueInfo500)
.frame(width: 206, height: 206)
}
if callViewModel.remoteAddress != nil {
let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!)
let contactAvatarModel = addressFriend != nil
? ContactsManager.shared.avatarListModel.first(where: {
($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy)
&& $0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
: ContactAvatarModel(friend: nil, name: "", withPresence: false)
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 200, hidePresence: true)
}
} else {
if callViewModel.remoteAddress!.displayName != nil {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.remoteAddress!.displayName!,
lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1
? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
} else {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.remoteAddress!.username ?? "Username Error",
lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1
? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
}
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
}
if callViewModel.isRemoteDeviceTrusted {
VStack {
Spacer()
HStack {
Image("trusted")
.resizable()
.frame(width: 25, height: 25)
.padding(.all, 15)
Spacer()
}
}
.frame(width: 200, height: 200)
}
}
Text(callViewModel.displayName)
.padding(.top)
.default_text_style_white(styleSize: 22)
Text(callViewModel.remoteAddressString)
.default_text_style_white_300(styleSize: 16)
Spacer()
}
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
core.nativeVideoWindow = view
}
}
.frame(
width:
angleDegree == 0
? (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8)
: (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom),
height:
angleDegree == 0
? (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom)
: (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8)
)
.scaledToFill()
.clipped()
.onTapGesture {
if telecomManager.remoteVideo {
fullscreenVideo.toggle()
}
}
if telecomManager.remoteVideo {
HStack {
Spacer()
VStack {
Spacer()
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
core.nativePreviewWindow = view
}
}
.frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2)
.cornerRadius(20)
.padding(10)
.padding(.trailing, abs(angleDegree/2))
}
}
.frame(
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
)
}
if callViewModel.isRecording {
HStack {
VStack {
Image("record-fill")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 32, height: 32)
.padding(10)
.if(fullscreenVideo && !telecomManager.isPausedByRemote) { view in
view.padding(.top, 30)
}
Spacer()
}
Spacer()
}
.frame(
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
)
}
if telecomManager.outgoingCallStarted {
VStack {
ActivityIndicator()
.frame(width: 20, height: 20)
.padding(.top, 60)
Text(callViewModel.counterToMinutes())
.onAppear {
callViewModel.timeElapsed = 0
}
.onReceive(callViewModel.timer) { _ in
callViewModel.timeElapsed = callViewModel.currentCall?.duration ?? 0
}
.onDisappear {
callViewModel.timeElapsed = 0
}
.padding(.top)
.foregroundStyle(.white)
Spacer()
}
.background(.clear)
.frame(
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
)
}
}
.frame(
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
)
.background(Color.gray900)
.cornerRadius(20)
.padding(.horizontal, fullscreenVideo && !telecomManager.isPausedByRemote ? 0 : 4)
.onRotate { newOrientation in
let oldOrientation = orientation
orientation = newOrientation
if orientation == .portrait || orientation == .portraitUpsideDown {
angleDegree = 0
} else {
if orientation == .landscapeLeft {
angleDegree = -90
} else if orientation == .landscapeRight {
angleDegree = 90
}
}
if (oldOrientation != orientation && oldOrientation != .faceUp) || (oldOrientation == .faceUp && (orientation == .landscapeLeft || orientation == .landscapeRight)) {
telecomManager.callStarted = false
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
telecomManager.callStarted = true
}
}
callViewModel.orientationUpdate(orientation: orientation)
}
.onAppear {
if orientation == .portrait && orientation == .portraitUpsideDown {
angleDegree = 0
} else {
if orientation == .landscapeLeft {
angleDegree = -90
} else if orientation == .landscapeRight {
angleDegree = 90
}
}
telecomManager.callStarted = false
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
telecomManager.callStarted = true
}
callViewModel.orientationUpdate(orientation: orientation)
}
simpleCallView(geometry: geometry)
Spacer()
}
@ -619,6 +402,396 @@ struct CallView: View {
}
}
func simpleCallView(geometry: GeometryProxy) -> some View {
ZStack {
if !callViewModel.isConference {
VStack {
Spacer()
ZStack {
if callViewModel.isRemoteDeviceTrusted {
Circle()
.fill(Color.blueInfo500)
.frame(width: 206, height: 206)
}
if callViewModel.remoteAddress != nil {
let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!)
let contactAvatarModel = addressFriend != nil
? ContactsManager.shared.avatarListModel.first(where: {
($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy)
&& $0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
: ContactAvatarModel(friend: nil, name: "", withPresence: false)
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 200, hidePresence: true)
}
} else {
if callViewModel.remoteAddress!.displayName != nil {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.remoteAddress!.displayName!,
lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1
? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
} else {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.remoteAddress!.username ?? "Username Error",
lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1
? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
}
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
}
if callViewModel.isRemoteDeviceTrusted {
VStack {
Spacer()
HStack {
Image("trusted")
.resizable()
.frame(width: 25, height: 25)
.padding(.all, 15)
Spacer()
}
}
.frame(width: 200, height: 200)
}
}
Text(callViewModel.displayName)
.padding(.top)
.default_text_style_white(styleSize: 22)
Text(callViewModel.remoteAddressString)
.default_text_style_white_300(styleSize: 16)
Spacer()
}
} else {
VStack {
Spacer()
ZStack {
if callViewModel.activeSpeakerParticipant?.address != nil {
let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.activeSpeakerParticipant!.address)
let contactAvatarModel = addressFriend != nil
? ContactsManager.shared.avatarListModel.first(where: {
($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy)
&& $0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly()
})
: ContactAvatarModel(friend: nil, name: "", withPresence: false)
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 200, hidePresence: true)
}
} else {
if callViewModel.activeSpeakerParticipant!.address.displayName != nil {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.activeSpeakerParticipant!.address.displayName!,
lastName: callViewModel.activeSpeakerParticipant!.address.displayName!.components(separatedBy: " ").count > 1
? callViewModel.activeSpeakerParticipant!.address.displayName!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
} else {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.activeSpeakerParticipant!.address.username ?? "Username Error",
lastName: callViewModel.activeSpeakerParticipant!.address.username!.components(separatedBy: " ").count > 1
? callViewModel.activeSpeakerParticipant!.address.username!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
}
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
}
}
Spacer()
}
}
if !callViewModel.isConference {
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
core.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
}
}
.frame(
width:
angleDegree == 0
? (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8)
: (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom),
height:
angleDegree == 0
? (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom)
: (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8)
)
.scaledToFill()
.clipped()
.onTapGesture {
if telecomManager.remoteVideo {
fullscreenVideo.toggle()
}
}
if telecomManager.remoteVideo {
HStack {
Spacer()
VStack {
Spacer()
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
core.nativePreviewWindow = view
}
}
.frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2)
.cornerRadius(20)
.padding(10)
.padding(.trailing, abs(angleDegree/2))
}
}
.frame(
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
)
}
} else {
/*
if !viewIsDisplayed {
VStack {
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
viewIsDisplayed = true
}
}
}
if viewIsDisplayed && (callViewModel.receiveVideo || telecomManager.remoteConfVideo) {
*/
if (callViewModel.receiveVideo || telecomManager.remoteConfVideo) {
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
//core.nativeVideoWindow = view
core.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
}
}
.frame(
width:
angleDegree == 0
? (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8)
: (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom),
height:
angleDegree == 0
? (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom)
: (fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8)
)
.scaledToFill()
.clipped()
.onTapGesture {
if telecomManager.remoteVideo {
fullscreenVideo.toggle()
}
}
HStack {
Spacer()
VStack {
Spacer()
ScrollView(.horizontal) {
HStack {
ForEach(0..<callViewModel.participantList.count - 1, id: \.self) { index in
ZStack {
VStack {
Spacer()
Avatar(contactAvatarModel: callViewModel.participantList[index].avatarModel, avatarSize: 50)
Spacer()
}
VStack(alignment: .leading) {
Spacer()
Text(callViewModel.participantList[index].name)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.white)
.default_text_style_500(styleSize: 14)
.lineLimit(1)
.padding(.horizontal, 10)
.padding(.bottom, 6)
}
.frame(maxWidth: .infinity)
}
.frame(width: 140, height: 140)
.background(Color.gray600)
.cornerRadius(20)
}
}
}
}
}
.frame(
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
)
.padding(.bottom, 10)
/*
HStack {
Spacer()
VStack {
Spacer()
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
core.nativePreviewWindow = view
}
}
.frame(width: angleDegree == 0 ? 120*1.2 : 160*1.2, height: angleDegree == 0 ? 160*1.2 : 120*1.2)
.cornerRadius(20)
.padding(10)
.padding(.trailing, abs(angleDegree/2))
}
}
.frame(
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
)
*/
}
}
if callViewModel.isRecording {
HStack {
VStack {
Image("record-fill")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 32, height: 32)
.padding(10)
.if(fullscreenVideo && !telecomManager.isPausedByRemote) { view in
view.padding(.top, 30)
}
Spacer()
}
Spacer()
}
.frame(
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
)
}
if telecomManager.outgoingCallStarted {
VStack {
ActivityIndicator()
.frame(width: 20, height: 20)
.padding(.top, 60)
Text(callViewModel.counterToMinutes())
.onAppear {
callViewModel.timeElapsed = 0
}
.onReceive(callViewModel.timer) { _ in
callViewModel.timeElapsed = callViewModel.currentCall?.duration ?? 0
}
.onDisappear {
callViewModel.timeElapsed = 0
}
.padding(.top)
.foregroundStyle(.white)
Spacer()
}
.background(.clear)
.frame(
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
)
}
}
.frame(
maxWidth: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.width : geometry.size.width - 8,
maxHeight: fullscreenVideo && !telecomManager.isPausedByRemote ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - (minBottomSheetHeight * geometry.size.height > 80 ? minBottomSheetHeight * geometry.size.height : 78) - 40 - 20 + geometry.safeAreaInsets.bottom
)
.background(Color.gray900)
.cornerRadius(20)
.padding(.horizontal, fullscreenVideo && !telecomManager.isPausedByRemote ? 0 : 4)
.onRotate { newOrientation in
let oldOrientation = orientation
orientation = newOrientation
if orientation == .portrait || orientation == .portraitUpsideDown {
angleDegree = 0
} else {
if orientation == .landscapeLeft {
angleDegree = -90
} else if orientation == .landscapeRight {
angleDegree = 90
}
}
if (oldOrientation != orientation && oldOrientation != .faceUp) || (oldOrientation == .faceUp && (orientation == .landscapeLeft || orientation == .landscapeRight)) {
telecomManager.callStarted = false
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
telecomManager.callStarted = true
}
}
callViewModel.orientationUpdate(orientation: orientation)
}
.onAppear {
if orientation == .portrait && orientation == .portraitUpsideDown {
angleDegree = 0
} else {
if orientation == .landscapeLeft {
angleDegree = -90
} else if orientation == .landscapeRight {
angleDegree = 90
}
}
telecomManager.callStarted = false
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
telecomManager.callStarted = true
}
callViewModel.orientationUpdate(orientation: orientation)
}
}
// swiftlint:disable function_body_length
func bottomSheetContent(geo: GeometryProxy) -> some View {
GeometryReader { _ in
VStack(spacing: 0) {
@ -658,22 +831,41 @@ struct CallView: View {
Spacer()
Button {
callViewModel.toggleVideo()
} label: {
HStack {
Image(telecomManager.remoteVideo ? "video-camera" : "video-camera-slash")
.renderingMode(.template)
.resizable()
.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white)
.frame(width: 32, height: 32)
if !callViewModel.isConference {
Button {
callViewModel.toggleVideo()
} label: {
HStack {
Image(telecomManager.remoteVideo ? "video-camera" : "video-camera-slash")
.renderingMode(.template)
.resizable()
.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? .white : Color.gray500)
.cornerRadius(40)
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
} else {
Button {
callViewModel.displayMyVideo()
} label: {
HStack {
Image(callViewModel.videoDisplayed ? "video-camera" : "video-camera-slash")
.renderingMode(.template)
.resizable()
.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? .white : Color.gray500)
.cornerRadius(40)
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? .white : Color.gray500)
.cornerRadius(40)
.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote)
Button {
callViewModel.toggleMuteMicrophone()
@ -695,8 +887,6 @@ struct CallView: View {
if AVAudioSession.sharedInstance().availableInputs != nil
&& !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty {
hideButtonsSheet = true
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
audioRouteSheet = true
}
@ -732,63 +922,109 @@ struct CallView: View {
if orientation != .landscapeLeft && orientation != .landscapeRight {
HStack(spacing: 0) {
VStack {
Button {
if callViewModel.calls.count < 2 {
if !callViewModel.isConference {
VStack {
Button {
if callViewModel.calls.count < 2 {
withAnimation {
callViewModel.isTransferInsteadCall = true
MagicSearchSingleton.shared.searchForSuggestions()
isShowStartCallFragment.toggle()
}
} else {
callViewModel.transferClicked()
}
} label: {
HStack {
Image("phone-transfer")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
Text(callViewModel.calls.count < 2 ? "Transfer" : "Attended transfer")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
}
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
VStack {
Button {
withAnimation {
callViewModel.isTransferInsteadCall = true
MagicSearchSingleton.shared.searchForSuggestions()
isShowStartCallFragment.toggle()
}
} else {
callViewModel.transferClicked()
}
} label: {
HStack {
Image("phone-transfer")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
} label: {
HStack {
Image("phone-plus")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
Text("New call")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
Text(callViewModel.calls.count < 2 ? "Transfer" : "Attended transfer")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
}
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
VStack {
Button {
withAnimation {
MagicSearchSingleton.shared.searchForSuggestions()
isShowStartCallFragment.toggle()
}
} label: {
HStack {
Image("phone-plus")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
} else {
VStack {
Button {
} label: {
HStack {
Image("screencast")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.gray500)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background(.white)
.cornerRadius(40)
.disabled(true)
Text("Partage d'écran")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
Text("New call")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
VStack {
Button {
} label: {
HStack {
Image("users")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
Text("Participants")
.foregroundStyle(.white)
.default_text_style(styleSize: 15)
}
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
}
.frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25)
VStack {
ZStack {
Button {
@ -1162,6 +1398,7 @@ struct CallView: View {
.frame(maxHeight: .infinity, alignment: .top)
}
}
// swiftlint:enable function_body_length
func getAudioRouteImage() {
imageAudioRoute = AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of Linphone
*
* 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 ParticipantModel: ObservableObject {
static let TAG = "[Participant Model]"
let address: Address
@Published var sipUri: String
@Published var name: String
@Published var avatarModel: ContactAvatarModel
init(address: Address) {
self.address = address
self.sipUri = address.asStringUriOnly()
let addressFriend = ContactsManager.shared.getFriendWithAddress(address: self.address)
var nameTmp = ""
if addressFriend != nil {
nameTmp = addressFriend!.name!
} else {
nameTmp = address.displayName != nil
? address.displayName!
: address.username!
}
self.name = nameTmp
self.avatarModel = addressFriend != nil
? ContactsManager.shared.avatarListModel.first(where: {
$0.friend!.name == addressFriend!.name
&& $0.friend!.address!.asStringUriOnly() == address.asStringUriOnly()
}) ?? ContactAvatarModel(friend: nil, name: nameTmp, withPresence: false)
: ContactAvatarModel(friend: nil, name: nameTmp, withPresence: false)
}
}

View file

@ -45,6 +45,14 @@ class CallViewModel: ObservableObject {
@Published var isRemoteDeviceTrusted: Bool = false
@Published var selectedCall: Call?
@Published var isTransferInsteadCall: Bool = false
@Published var isConference: Bool = false
@Published var videoDisplayed: Bool = false
@Published var receiveVideo: Bool = false
@Published var participantList: [ParticipantModel] = []
@Published var activeSpeakerParticipant: ParticipantModel? = nil
private var mConferenceSuscriptions = Set<AnyCancellable?>()
var calls: [Call] = []
@ -110,6 +118,7 @@ class CallViewModel: ObservableObject {
self.isRemoteDeviceTrusted = self.telecomManager.callInProgress ? isDeviceTrusted : false
self.getCallsList()
self.getConference()
}
self.callSuscriptions.insert(self.currentCall!.publisher?.onEncryptionChanged?.postOnMainQueue {(cbVal: (call: Call, on: Bool, authenticationToken: String?)) in
@ -127,6 +136,43 @@ class CallViewModel: ObservableObject {
}
}
func getConference() {
coreContext.doOnCoreQueue { core in
//conf = self.currentCall?.conference != nil ? self.currentCall!.conference! : core.findConferenceInformationFromUri(uri: (self.currentCall?.remoteContactAddress)!)
if self.currentCall?.remoteContactAddress != nil {
let conf = core.findConferenceInformationFromUri(uri: (self.currentCall?.remoteContactAddress)!)
DispatchQueue.main.async {
self.isConference = conf != nil
if self.isConference {
self.displayName = conf?.subject ?? ""
self.participantList = []
conf?.participantInfos.forEach({ participantInfo in
if participantInfo.address != nil {
self.participantList.append(ParticipantModel(address: participantInfo.address!))
}
})
self.addConferenceCallBacks()
}
}
}
}
}
func addConferenceCallBacks() {
coreContext.doOnCoreQueue { core in
self.mConferenceSuscriptions.insert(
self.currentCall?.conference?.publisher?.onActiveSpeakerParticipantDevice?.postOnMainQueue {(cbValue: (conference: Conference, participantDevice: ParticipantDevice)) in
let direction = cbValue.participantDevice.getStreamCapability(streamType: StreamType.Video)
self.receiveVideo = direction == MediaDirection.SendRecv || direction == MediaDirection.SendOnly
if cbValue.participantDevice.address != nil {
self.activeSpeakerParticipant = ParticipantModel(address: cbValue.participantDevice.address!)
}
})
}
}
func terminateCall() {
coreContext.doOnCoreQueue { core in
if self.currentCall != nil {
@ -186,6 +232,33 @@ class CallViewModel: ObservableObject {
}
}
func displayMyVideo() {
coreContext.doOnCoreQueue { core in
if self.currentCall != nil {
do {
let params = try core.createCallParams(call: self.currentCall)
if params.videoEnabled {
if params.videoDirection == MediaDirection.SendRecv {
params.videoDirection = MediaDirection.RecvOnly
} else if params.videoDirection == MediaDirection.RecvOnly {
params.videoDirection = MediaDirection.SendRecv
}
}
try self.currentCall!.update(params: params)
let video = params.videoDirection == MediaDirection.SendRecv
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.videoDisplayed = video
}
} catch {
}
}
}
}
func switchCamera() {
coreContext.doOnCoreQueue { core in
let currentDevice = core.videoDevice

View file

@ -46,7 +46,6 @@ class ConversationsListViewModel: ObservableObject {
var conversationsListTmp: [ConversationModel] = []
chatRooms.forEach { chatRoom in
//let disabledBecauseNotSecured = (account?.isInSecureMode() == true && !chatRoom.hasCapability) ? Capabilities.Encrypted.toInt() : 0
if chatRoom.hasCapability(mask: ChatRoom.Capabilities.OneToOne.rawValue) {
}
@ -54,32 +53,6 @@ class ConversationsListViewModel: ObservableObject {
let model = ConversationModel(chatRoom: chatRoom)
conversationsListTmp.append(model)
}
/*
else {
val participants = chatRoom.participants
val found = participants.find {
// Search in address but also in contact name if exists
val model =
coreContext.contactsManager.getContactAvatarModelForAddress(it.address)
model.contactName?.contains(
filter,
ignoreCase = true
) == true || it.address.asStringUriOnly().contains(
filter,
ignoreCase = true
)
}
if (
found != null ||
chatRoom.peerAddress.asStringUriOnly().contains(filter, ignoreCase = true) ||
chatRoom.subject.orEmpty().contains(filter, ignoreCase = true)
) {
val model = ConversationModel(chatRoom, disabledBecauseNotSecured)
list.add(model)
count += 1
}
}
*/
}
if !self.conversationsList.isEmpty {

View file

@ -80,7 +80,7 @@ struct Avatar: View {
? contactAvatarModel.name.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 50, height: 50)
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
} else {
Image("profil-picture-default")