Add Meeting Waiting Room

This commit is contained in:
Benoit Martins 2024-04-10 17:24:19 +02:00
parent 0299640c2c
commit 601be3ebed
13 changed files with 908 additions and 51 deletions

View file

@ -63,6 +63,8 @@
D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */; };
D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */; };
D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */; };
D73449992BC6932A00778C56 /* MeetingWaitingRoomFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73449982BC6932A00778C56 /* MeetingWaitingRoomFragment.swift */; };
D734499B2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */; };
D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */; };
D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */; };
D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */; };
@ -200,6 +202,8 @@
D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListFragment.swift; sourceTree = "<group>"; };
D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListViewModel.swift; sourceTree = "<group>"; };
D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListBottomSheet.swift; sourceTree = "<group>"; };
D73449982BC6932A00778C56 /* MeetingWaitingRoomFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingWaitingRoomFragment.swift; sourceTree = "<group>"; };
D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingWaitingRoomViewModel.swift; sourceTree = "<group>"; };
D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = "<group>"; };
D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = "<group>"; };
D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage1Fragment.swift; sourceTree = "<group>"; };
@ -610,6 +614,7 @@
D720E6AB2BAD81C800DDFD87 /* Model */,
D7B99E972B29B37F00BE7BF2 /* ViewModel */,
D7B5678D2B28888F00DE63EB /* CallView.swift */,
D73449982BC6932A00778C56 /* MeetingWaitingRoomFragment.swift */,
);
path = Call;
sourceTree = "<group>";
@ -618,6 +623,7 @@
isa = PBXGroup;
children = (
D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */,
D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -890,6 +896,7 @@
D70A26F02B7D02E6006CC8FC /* ConversationViewModel.swift in Sources */,
D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */,
D7E6D04D2AEBD77600A57AAF /* CustomBottomSheet.swift in Sources */,
D73449992BC6932A00778C56 /* MeetingWaitingRoomFragment.swift in Sources */,
D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */,
66C491FF2B24D4AC00CEA16D /* FileUtils.swift in Sources */,
D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */,
@ -898,6 +905,7 @@
D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */,
D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */,
D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */,
D734499B2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift in Sources */,
D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */,
D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */,
D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */,

View file

@ -109,6 +109,7 @@ final class CoreContext: ObservableObject {
self.mCore.videoCaptureEnabled = true
self.mCore.videoDisplayEnabled = true
self.mCore.videoPreviewEnabled = false
self.mCoreSuscriptions.insert(self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in
if cbVal.state == GlobalState.On {

View file

@ -76,6 +76,7 @@ struct LinphoneApp: App {
@State private var historyListViewModel: HistoryListViewModel?
@State private var startCallViewModel: StartCallViewModel?
@State private var callViewModel: CallViewModel?
@State private var meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel?
@State private var conversationsListViewModel: ConversationsListViewModel?
@State private var conversationViewModel: ConversationViewModel?
@ -99,6 +100,7 @@ struct LinphoneApp: App {
&& historyListViewModel != nil
&& startCallViewModel != nil
&& callViewModel != nil
&& meetingWaitingRoomViewModel != nil
&& conversationsListViewModel != nil
&& conversationViewModel != nil {
ContentView(
@ -108,6 +110,7 @@ struct LinphoneApp: App {
historyListViewModel: historyListViewModel!,
startCallViewModel: startCallViewModel!,
callViewModel: callViewModel!,
meetingWaitingRoomViewModel: meetingWaitingRoomViewModel!,
conversationsListViewModel: conversationsListViewModel!,
conversationViewModel: conversationViewModel!
)
@ -123,6 +126,7 @@ struct LinphoneApp: App {
historyListViewModel = HistoryListViewModel()
startCallViewModel = StartCallViewModel()
callViewModel = CallViewModel()
meetingWaitingRoomViewModel = MeetingWaitingRoomViewModel()
conversationsListViewModel = ConversationsListViewModel()
conversationViewModel = ConversationViewModel()
}

View file

@ -241,6 +241,9 @@
},
"Conditions de service" : {
},
"Connexion à la réunion" : {
},
"Contacts" : {
@ -443,6 +446,9 @@
},
"Marquer comme non lu" : {
},
"Mercredi 10 Avril 2024 | 2:41 PM - 2:42 PM" : {
},
"Message" : {
@ -553,6 +559,9 @@
},
"Register" : {
},
"Rejoindre" : {
},
"Remove from favourites" : {
@ -696,6 +705,9 @@
},
"Vos communications sont en sécurité grâce aux **Chiffrement de bout en bout**." : {
},
"Vous allez rejoindre la réunion dans quelques instants..." : {
},
"Welcome" : {

View file

@ -329,7 +329,7 @@ extension ProviderDelegate: CXProviderDelegate {
CoreContext.shared.doOnCoreQueue { core in
do {
core.configureAudioSession()
try TelecomManager.shared.doCall(core: core, addr: addr!, isSas: callInfo?.sasEnabled ?? false, isVideo: callInfo?.videoEnabled ?? false, isConference: callInfo?.isConference ?? false)
try TelecomManager.shared.doCall(core: core, addr: addr!, isSas: callInfo?.sasEnabled ?? false, isVideo: ((callInfo?.videoEnabled ?? false) && core.videoPreviewEnabled), isConference: callInfo?.isConference ?? false)
action.fulfill()
} catch {
Log.info("CallKit: Call started failed because \(error)")

View file

@ -50,6 +50,9 @@ class TelecomManager: ObservableObject {
@Published var refreshCallViewModel: Bool = false
@Published var remainingCall: Bool = false
@Published var callConnected: Bool = false
@Published var meetingWaitingRoomDisplayed: Bool = false
@Published var meetingWaitingRoomSelected: Address?
@Published var meetingWaitingRoomName: String = ""
var actionToFulFill: CXCallAction?
var callkitAudioSessionActivated: Bool?
@ -215,6 +218,7 @@ class TelecomManager: ObservableObject {
if isConference {
lcallParams.videoEnabled = true
lcallParams.videoDirection = isVideo ? MediaDirection.SendRecv : MediaDirection.RecvOnly
/* if (ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! != .AudioOnly) {
lcallParams.videoEnabled = true
lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly
@ -393,6 +397,7 @@ class TelecomManager: ObservableObject {
}
}
/*
if self.remoteConfVideo && self.remoteConfVideo != oldRemoteConfVideo {
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
@ -400,6 +405,7 @@ class TelecomManager: ObservableObject {
}
}
*/
if self.remoteConfVideo {
Log.info("[Call] Remote video is activated")
@ -444,8 +450,11 @@ class TelecomManager: ObservableObject {
self.isPausedByRemote = false
}
if (cstate == Call.State.Connected) {
if cstate == Call.State.Connected {
self.callConnected = true
self.meetingWaitingRoomSelected = nil
self.meetingWaitingRoomDisplayed = false
}
}

View file

@ -538,7 +538,7 @@ struct CallView: View {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 200, hidePresence: true)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
displayVideo = true
}
}
@ -554,7 +554,7 @@ struct CallView: View {
.frame(width: 200, height: 200)
.clipShape(Circle())
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
displayVideo = true
}
}
@ -569,7 +569,7 @@ struct CallView: View {
.frame(width: 200, height: 200)
.clipShape(Circle())
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
displayVideo = true
}
}
@ -750,7 +750,7 @@ struct CallView: View {
if telecomManager.outgoingCallStarted {
VStack {
ActivityIndicator()
ActivityIndicator(color: .white)
.frame(width: 20, height: 20)
.padding(.top, 60)
@ -772,7 +772,8 @@ struct CallView: View {
}
.onDisappear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
callViewModel.getConference()
// callViewModel.getConference()
callViewModel.waitingForCreatedStateConference()
}
}
.background(.clear)
@ -799,6 +800,8 @@ struct CallView: View {
angleDegree = -90
} else if orientation == .landscapeRight {
angleDegree = 90
} else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
angleDegree = 90
}
}
@ -820,6 +823,8 @@ struct CallView: View {
angleDegree = -90
} else if orientation == .landscapeRight {
angleDegree = 90
} else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
angleDegree = 90
}
}

View file

@ -0,0 +1,542 @@
/*
* 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 SwiftUI
import linphonesw
import AVFAudio
struct MeetingWaitingRoomFragment: View {
@ObservedObject private var coreContext = CoreContext.shared
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject var meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@State private var orientation = UIDevice.current.orientation
let pub = NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
@State var audioRouteSheet: Bool = false
@State var options: Int = 1
@State var angleDegree = 0.0
var body: some View {
GeometryReader { geometry in
if #available(iOS 16.0, *), idiom != .pad {
innerView(geometry: geometry)
.sheet(isPresented: $audioRouteSheet, onDismiss: {
audioRouteSheet = false
}) {
innerBottomSheet()
.presentationDetents([.fraction(0.3)])
}
.onAppear {
meetingWaitingRoomViewModel.enableAVAudioSession()
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
} catch _ {
}
}
.onDisappear {
meetingWaitingRoomViewModel.disableAVAudioSession()
}
} else {
innerView(geometry: geometry)
.halfSheet(showSheet: $audioRouteSheet) {
innerBottomSheet()
} onDismiss: {
audioRouteSheet = false
}
.onAppear {
meetingWaitingRoomViewModel.enableAVAudioSession()
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
} catch _ {
}
}
.onDisappear {
meetingWaitingRoomViewModel.disableAVAudioSession()
}
}
}
}
@ViewBuilder
func innerView(geometry: GeometryProxy) -> some View {
VStack {
if #available(iOS 16.0, *) {
Rectangle()
.foregroundColor(Color.orangeMain500)
.edgesIgnoringSafeArea(.top)
.frame(height: 0)
} else if idiom != .pad && !(orientation == .landscapeLeft || orientation == .landscapeRight
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) {
Rectangle()
.foregroundColor(Color.orangeMain500)
.edgesIgnoringSafeArea(.top)
.frame(height: 1)
}
HStack {
Button {
withAnimation {
meetingWaitingRoomViewModel.disableVideoPreview()
telecomManager.meetingWaitingRoomSelected = nil
telecomManager.meetingWaitingRoomDisplayed = false
}
} label: {
Image("caret-left")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
Text(telecomManager.meetingWaitingRoomName)
.default_text_style_white_800(styleSize: 16)
Spacer()
}
.frame(height: 40)
.zIndex(1)
HStack {
Button {
} label: {
Image("caret-left")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeMain500)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
.hidden()
Text("Mercredi 10 Avril 2024 | 2:41 PM - 2:42 PM")
.foregroundStyle(.white)
.default_text_style_white(styleSize: 12)
Spacer()
}
.frame(height: 40)
.padding(.top, -25)
.zIndex(1)
if !telecomManager.callStarted {
ZStack {
VStack {
Spacer()
if meetingWaitingRoomViewModel.avatarDisplayed {
ZStack {
if meetingWaitingRoomViewModel.isRemoteDeviceTrusted {
Circle()
.fill(Color.blueInfo500)
.frame(width: 206, height: 206)
}
if meetingWaitingRoomViewModel.avatarModel != nil {
Avatar(contactAvatarModel: meetingWaitingRoomViewModel.avatarModel!, avatarSize: 200, hidePresence: true)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
}
if meetingWaitingRoomViewModel.isRemoteDeviceTrusted {
VStack {
Spacer()
HStack {
Image("trusted")
.resizable()
.frame(width: 25, height: 25)
.padding(.all, 15)
Spacer()
}
}
.frame(width: 200, height: 200)
}
}
}
Spacer()
}
.frame(
width:
angleDegree == 0
? 120 * ceil((geometry.size.width - 20) / 120)
: 160 * ceil((geometry.size.height - 160) / 160),
height:
angleDegree == 0
? 160 * ceil((geometry.size.width - 20) / 120)
: 120 * ceil((geometry.size.height - 160) / 160)
)
LinphoneVideoViewHolder { view in
coreContext.doOnCoreQueue { core in
core.nativePreviewWindow = view
}
}
.frame(
width:
angleDegree == 0
? 120 * ceil((geometry.size.width - 20) / 120)
: 160 * ceil((geometry.size.height - 160) / 160),
height:
angleDegree == 0
? 160 * ceil((geometry.size.width - 20) / 120)
: 120 * ceil((geometry.size.height - 160) / 160)
)
VStack {
HStack {
Spacer()
if meetingWaitingRoomViewModel.videoDisplayed {
Button {
meetingWaitingRoomViewModel.switchCamera()
} label: {
Image("camera-rotate")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 30, height: 30)
}
}
}
Spacer()
HStack {
Text(meetingWaitingRoomViewModel.userName)
.foregroundStyle(.white)
.default_text_style_white(styleSize: 20)
Spacer()
}
}
.padding(.all, 10)
.frame(maxWidth: geometry.size.width - 20, maxHeight: geometry.size.height - (angleDegree == 0 ? 250 : 160))
}
.background(Color.gray600)
.frame(maxWidth: geometry.size.width - 20, maxHeight: geometry.size.height - (angleDegree == 0 ? 250 : 160))
.cornerRadius(20)
.padding(.horizontal, 10)
.onDisappear {
meetingWaitingRoomViewModel.disableVideoPreview()
}
if angleDegree != 0 {
Spacer()
}
HStack {
Spacer()
Button {
!meetingWaitingRoomViewModel.videoDisplayed ? meetingWaitingRoomViewModel.enableVideoPreview() : meetingWaitingRoomViewModel.disableVideoPreview()
} label: {
HStack {
Image(meetingWaitingRoomViewModel.videoDisplayed ? "video-camera" : "video-camera-slash")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
.padding(.horizontal, 5)
Button {
meetingWaitingRoomViewModel.toggleMuteMicrophone()
} label: {
HStack {
Image(meetingWaitingRoomViewModel.micMutted ? "microphone-slash" : "microphone")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
.padding(.horizontal, 5)
Button {
if AVAudioSession.sharedInstance().availableInputs != nil
&& !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty {
audioRouteSheet = true
} else {
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(
AVAudioSession.sharedInstance().currentRoute.outputs.filter(
{ $0.portType.rawValue == "Speaker" }
).isEmpty ? .speaker : .none
)
} catch _ {
}
}
} label: {
HStack {
Image(meetingWaitingRoomViewModel.imageAudioRoute)
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
.onAppear(perform: meetingWaitingRoomViewModel.getAudioRouteImage)
.onReceive(pub) { _ in
self.meetingWaitingRoomViewModel.getAudioRouteImage()
}
}
}
.buttonStyle(PressedButtonStyle())
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
.padding(.horizontal, 5)
Spacer()
if angleDegree != 0 {
Button(action: {
meetingWaitingRoomViewModel.joinMeeting()
}, label: {
Text("Rejoindre")
.default_text_style_white_600(styleSize: 20)
.frame(height: 35)
.frame(maxWidth: .infinity)
})
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.orangeMain500)
.cornerRadius(60)
.padding(.horizontal, 10)
.frame(width: (geometry.size.width - 20) / 2)
}
}
.padding(.all, 10)
if angleDegree == 0 {
Spacer()
Button(action: {
meetingWaitingRoomViewModel.joinMeeting()
}, label: {
Text("Rejoindre")
.default_text_style_white_600(styleSize: 20)
.frame(height: 35)
.frame(maxWidth: .infinity)
})
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.orangeMain500)
.cornerRadius(60)
.padding(.bottom)
.padding(.horizontal, 10)
}
} else {
VStack {
Spacer()
Text("Connexion à la réunion")
.default_text_style_white_600(styleSize: 24)
.multilineTextAlignment(.center)
.padding(.bottom, 10)
Text("Vous allez rejoindre la réunion dans quelques instants...")
.default_text_style_white(styleSize: 16)
.multilineTextAlignment(.center)
.padding(.bottom, 20)
ActivityIndicator(color: Color.orangeMain500)
.frame(width: 35, height: 35)
Spacer()
}
}
Spacer()
}
.background(Color.gray900)
.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
} else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
angleDegree = 90
}
}
meetingWaitingRoomViewModel.orientationUpdate(orientation: orientation)
}
.onAppear {
if orientation == .portrait || orientation == .portraitUpsideDown {
angleDegree = 0
} else {
if orientation == .landscapeLeft {
angleDegree = -90
} else if orientation == .landscapeRight {
angleDegree = 90
} else if UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height {
angleDegree = 90
}
}
meetingWaitingRoomViewModel.orientationUpdate(orientation: orientation)
}
}
@ViewBuilder
func innerBottomSheet() -> some View {
VStack(spacing: 0) {
Button(action: {
options = 1
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
if meetingWaitingRoomViewModel.isHeadPhoneAvailable() {
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Receiver") }).first)
} else {
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.first)
}
} catch _ {
}
}, label: {
HStack {
Image(options == 1 ? "radio-button-fill" : "radio-button")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text(!meetingWaitingRoomViewModel.isHeadPhoneAvailable() ? "Earpiece" : "Headphones")
.default_text_style_white(styleSize: 15)
Spacer()
Image(!meetingWaitingRoomViewModel.isHeadPhoneAvailable() ? "ear" : "headset")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
})
.frame(maxHeight: .infinity)
Button(action: {
options = 2
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
} catch _ {
}
}, label: {
HStack {
Image(options == 2 ? "radio-button-fill" : "radio-button")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("Speaker")
.default_text_style_white(styleSize: 15)
Spacer()
Image("speaker-high")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
})
.frame(maxHeight: .infinity)
Button(action: {
options = 3
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Bluetooth") }).first)
} catch _ {
}
}, label: {
HStack {
Image(options == 3 ? "radio-button-fill" : "radio-button")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
Text("Bluetooth")
.default_text_style_white(styleSize: 15)
Spacer()
Image("bluetooth")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 25, height: 25, alignment: .leading)
.padding(.all, 10)
}
})
.frame(maxHeight: .infinity)
}
.padding(.horizontal, 20)
.background(Color.gray600)
.frame(maxHeight: .infinity)
}
}
#Preview {
MeetingWaitingRoomFragment(meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel())
}

View file

@ -53,7 +53,6 @@ class CallViewModel: ObservableObject {
@Published var activeSpeakerName: String = ""
@Published var myParticipantModel: ParticipantModel? = nil
private var mConferenceSuscriptions = Set<AnyCancellable?>()
var calls: [Call] = []
@ -98,19 +97,10 @@ class CallViewModel: ObservableObject {
self.remoteAddressString = String(self.currentCall!.remoteAddress!.asStringUriOnly().dropFirst(4))
self.remoteAddress = self.currentCall!.remoteAddress!
let friend = ContactsManager.shared.getFriendWithAddress(address: self.currentCall!.remoteAddress!)
if friend != nil && friend!.address != nil && friend!.address!.displayName != nil {
self.displayName = friend!.address!.displayName!
} else {
if self.currentCall!.remoteAddress!.displayName != nil {
self.displayName = self.currentCall!.remoteAddress!.displayName!
} else if self.currentCall!.remoteAddress!.username != nil {
self.displayName = self.currentCall!.remoteAddress!.username!
}
}
self.displayName = self.currentCall?.conference?.subject ?? ""
//self.avatarModel = ???
self.micMutted = self.currentCall!.microphoneMuted
self.micMutted = self.currentCall!.microphoneMuted || !core.micEnabled
self.isRecording = self.currentCall!.params!.isRecording
self.isPaused = self.isCallPaused()
self.timeElapsed = self.currentCall?.duration ?? 0
@ -128,7 +118,7 @@ class CallViewModel: ObservableObject {
}
self.getCallsList()
self.getConference()
self.waitingForCreatedStateConference()
}
self.callSuscriptions.insert(self.currentCall!.publisher?.onEncryptionChanged?.postOnMainQueue {(cbVal: (call: Call, on: Bool, authenticationToken: String?)) in
@ -157,19 +147,15 @@ class CallViewModel: ObservableObject {
if self.currentCall?.callLog?.localAddress != nil {
self.myParticipantModel = ParticipantModel(address: self.currentCall!.callLog!.localAddress!)
print("ParticipantModelParticipantModel myParticipantModel \(self.currentCall!.callLog!.localAddress!.asStringUriOnly())")
print("ParticipantModelParticipantModel 1 \(conf.me?.address!.asStringUriOnly())")
}
if conf.activeSpeakerParticipantDevice?.address != nil {
self.activeSpeakerParticipant = ParticipantModel(address: conf.activeSpeakerParticipantDevice!.address!)
print("ParticipantModelParticipantModel activeSpeakerParticipantDevice 1 \(conf.activeSpeakerParticipantDevice!.address!.asStringUriOnly())")
print("ParticipantModelParticipantModel 2 \(conf.activeSpeakerParticipantDevice!.address!.asStringUriOnly())")
} else if conf.participantList.first?.address != nil {
self.activeSpeakerParticipant = ParticipantModel(address: conf.participantList.first!.address!)
print("ParticipantModelParticipantModel activeSpeakerParticipantDevice 2 \(conf.participantList.first!.address!.asStringUriOnly())")
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.getConference()
}
print("ParticipantModelParticipantModel 3 \(conf.participantList.first!.address!.asStringUriOnly())")
}
if self.activeSpeakerParticipant != nil {
@ -187,7 +173,7 @@ class CallViewModel: ObservableObject {
conf.participantDeviceList.forEach({ participantDevice in
self.participantList.append(ParticipantModel(address: participantDevice.address!))
print("ParticipantModelParticipantModel participantDevice \(participantDevice.address!.asStringUriOnly())")
print("ParticipantModelParticipantModel 4 \(participantDevice.address!.asStringUriOnly()) \(conf.isIn) \(conf.state) \(self.currentCall?.state)")
})
//self.addConferenceCallBacks()
@ -198,6 +184,17 @@ class CallViewModel: ObservableObject {
}
}
func waitingForCreatedStateConference() {
self.mConferenceSuscriptions.insert(
self.currentCall?.conference?.publisher?.onStateChanged?.postOnMainQueue {(cbValue: (conference: Conference, state: Conference.State)) in
if cbValue.state == .Created {
print("ParticipantModelParticipantModel 0 \(cbValue.conference.isIn) \(cbValue.conference.state) \(self.currentCall?.state) \(cbValue.state)")
self.getConference()
}
}
)
}
func addConferenceCallBacks() {
coreContext.doOnCoreQueue { core in
self.mConferenceSuscriptions.insert(
@ -298,10 +295,16 @@ class CallViewModel: ObservableObject {
}
func toggleMuteMicrophone() {
coreContext.doOnCoreQueue { _ in
coreContext.doOnCoreQueue { core in
if self.currentCall != nil {
self.currentCall!.microphoneMuted = !self.currentCall!.microphoneMuted
self.micMutted = self.currentCall!.microphoneMuted
if !core.micEnabled && !self.currentCall!.microphoneMuted {
core.micEnabled = true
} else {
self.currentCall!.microphoneMuted = !self.currentCall!.microphoneMuted
}
self.micMutted = self.currentCall!.microphoneMuted || !core.micEnabled
Log.info(
"[CallViewModel] Microphone mute switch \(self.micMutted)"
)
@ -436,18 +439,6 @@ class CallViewModel: ObservableObject {
return false
}
func getAudioRoute() -> Int {
if AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty {
if AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty {
return 1
} else {
return 3
}
} else {
return 2
}
}
func orientationUpdate(orientation: UIDeviceOrientation) {
coreContext.doOnCoreQueue { core in
let oldLinphoneOrientation = core.deviceRotation

View file

@ -0,0 +1,261 @@
/*
* 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
import SwiftUI
import AVFAudio
class MeetingWaitingRoomViewModel: ObservableObject {
var coreContext = CoreContext.shared
var telecomManager = TelecomManager.shared
@Published var userName: String = ""
@Published var avatarModel: ContactAvatarModel?
@Published var micMutted: Bool = false
@Published var isRemoteDeviceTrusted: Bool = false
@Published var selectedCall: Call?
@Published var isConference: Bool = false
@Published var videoDisplayed: Bool = false
@Published var avatarDisplayed: Bool = true
@Published var imageAudioRoute: String = ""
init() {
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
} catch _ {
}
if !telecomManager.callStarted {
self.resetMeetingRoomView()
}
}
func resetMeetingRoomView() {
if self.telecomManager.meetingWaitingRoomSelected != nil {
coreContext.doOnCoreQueue { core in
let conf = core.findConferenceInformationFromUri(uri: self.telecomManager.meetingWaitingRoomSelected!)
if conf != nil && conf!.uri != nil {
let confNameTmp = conf?.subject ?? "Conference"
var userNameTmp = ""
let friend = core.defaultAccount != nil && core.defaultAccount!.contactAddress != nil
? ContactsManager.shared.getFriendWithAddress(address: core.defaultAccount!.contactAddress!)
: nil
if friend != nil && friend!.address != nil && friend!.address!.displayName != nil {
userNameTmp = friend!.address!.displayName!
} else {
if core.defaultAccount!.contactAddress!.displayName != nil {
userNameTmp = core.defaultAccount!.contactAddress!.displayName!
} else if core.defaultAccount!.contactAddress!.username != nil {
userNameTmp = core.defaultAccount!.contactAddress!.username!
}
}
let avatarModelTmp = friend != nil
? ContactsManager.shared.avatarListModel.first(where: {
$0.friend!.name == friend!.name
&& $0.friend!.address!.asStringUriOnly() == core.defaultAccount!.contactAddress!.asStringUriOnly()
}) ?? ContactAvatarModel(friend: nil, name: userNameTmp, withPresence: false)
: ContactAvatarModel(friend: nil, name: userNameTmp, withPresence: false)
if core.videoEnabled && !core.videoPreviewEnabled {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
core.videoPreviewEnabled = true
self.videoDisplayed = true
}
}
core.micEnabled = true
let micMuttedTmp = !core.micEnabled
DispatchQueue.main.async {
if self.telecomManager.meetingWaitingRoomName.isEmpty || self.telecomManager.meetingWaitingRoomName != confNameTmp {
self.telecomManager.meetingWaitingRoomName = confNameTmp
}
self.userName = userNameTmp
self.avatarModel = avatarModelTmp
self.micMutted = micMuttedTmp
}
}
}
}
}
func enableVideoPreview() {
self.coreContext.doOnCoreQueue { core in
if core.videoEnabled {
self.videoDisplayed = true
core.videoPreviewEnabled = true
}
}
}
func disableVideoPreview() {
coreContext.doOnCoreQueue { core in
if core.videoEnabled {
self.videoDisplayed = false
core.videoPreviewEnabled = false
}
}
}
func switchCamera() {
coreContext.doOnCoreQueue { core in
let currentDevice = core.videoDevice
Log.info("[CallViewModel] Current camera device is \(currentDevice)")
core.videoDevicesList.forEach { camera in
if camera != currentDevice && camera != "StaticImage: Static picture" {
Log.info("[CallViewModel] New camera device will be \(camera)")
do {
try core.setVideodevice(newValue: camera)
} catch _ {
}
}
}
}
}
func orientationUpdate(orientation: UIDeviceOrientation) {
coreContext.doOnCoreQueue { core in
let oldLinphoneOrientation = core.deviceRotation
var newRotation = 0
switch orientation {
case .portrait:
newRotation = 0
case .portraitUpsideDown:
newRotation = 180
case .landscapeRight:
newRotation = 90
case .landscapeLeft:
newRotation = 270
default:
newRotation = oldLinphoneOrientation
}
if oldLinphoneOrientation != newRotation {
core.deviceRotation = newRotation
}
}
}
func enableMicrophone() {
self.micMutted = false
}
func toggleMuteMicrophone() {
self.micMutted = !self.micMutted
}
func enableAVAudioSession() {
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch _ {
}
}
func disableAVAudioSession() {
do {
try AVAudioSession.sharedInstance().setActive(false)
} catch _ {
}
}
func getAudioRouteImage() {
print("AVAudioSessionAVAudioSession getAudioRouteImage \(imageAudioRoute)")
imageAudioRoute = AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty
? (
AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty
? (
isHeadPhoneAvailable()
? "headset"
: "speaker-slash"
)
: "bluetooth"
)
: "speaker-high"
print("AVAudioSessionAVAudioSession getAudioRouteImage \(imageAudioRoute)")
}
func isHeadPhoneAvailable() -> Bool {
guard let availableInputs = AVAudioSession.sharedInstance().availableInputs else {return false}
for inputDevice in availableInputs {
if inputDevice.portType == .headsetMic || inputDevice.portType == .headphones {
return true
}
}
return false
}
func joinMeeting() {
if self.telecomManager.meetingWaitingRoomSelected != nil {
if self.micMutted {
coreContext.doOnCoreQueue { core in
core.micEnabled = false
}
}
let audioSession = imageAudioRoute
telecomManager.doCallWithCore(
addr: self.telecomManager.meetingWaitingRoomSelected!, isVideo: self.videoDisplayed, isConference: true
)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
switch audioSession {
case "bluetooth":
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Bluetooth") }).first)
} catch _ {
}
case "speaker-high":
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
} catch _ {
}
default:
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
if self.isHeadPhoneAvailable() {
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Receiver") }).first)
} else {
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.first)
}
} catch _ {
}
}
}
}
}
}

View file

@ -40,6 +40,7 @@ struct ContentView: View {
@ObservedObject var historyListViewModel: HistoryListViewModel
@ObservedObject var startCallViewModel: StartCallViewModel
@ObservedObject var callViewModel: CallViewModel
@ObservedObject var meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel
@ObservedObject var conversationsListViewModel: ConversationsListViewModel
@ObservedObject var conversationViewModel: ConversationViewModel
@ -713,7 +714,7 @@ struct ContentView: View {
isShowDismissPopup: $isShowDismissPopup
)
.zIndex(3)
.transition(.move(edge: .bottom))
.transition(.opacity.combined(with: .move(edge: .bottom)))
.onAppear {
contactViewModel.indexDisplayedFriend = nil
}
@ -729,7 +730,7 @@ struct ContentView: View {
resetCallView: {callViewModel.resetCallView()}
)
.zIndex(4)
.transition(.move(edge: .bottom))
.transition(.opacity.combined(with: .move(edge: .bottom)))
.sheet(isPresented: $showingDialer) {
DialerBottomSheet(
startCallViewModel: startCallViewModel,
@ -748,7 +749,7 @@ struct ContentView: View {
resetCallView: {callViewModel.resetCallView()}
)
.zIndex(4)
.transition(.move(edge: .bottom))
.transition(.opacity.combined(with: .move(edge: .bottom)))
.halfSheet(showSheet: $showingDialer) {
DialerBottomSheet(
startCallViewModel: startCallViewModel,
@ -856,7 +857,16 @@ struct ContentView: View {
}
}
if telecomManager.callDisplayed && ((telecomManager.callInProgress && telecomManager.outgoingCallStarted) || telecomManager.callConnected) {
if telecomManager.meetingWaitingRoomDisplayed {
MeetingWaitingRoomFragment(meetingWaitingRoomViewModel: meetingWaitingRoomViewModel)
.zIndex(3)
.transition(.opacity.combined(with: .move(edge: .bottom)))
.onAppear {
meetingWaitingRoomViewModel.resetMeetingRoomView()
}
}
if telecomManager.callDisplayed && ((telecomManager.callInProgress && telecomManager.outgoingCallStarted) || telecomManager.callConnected) && !telecomManager.meetingWaitingRoomDisplayed {
CallView(callViewModel: callViewModel, fullscreenVideo: $fullscreenVideo, isShowCallsListFragment: $isShowCallsListFragment, isShowStartCallFragment: $isShowStartCallFragment)
.zIndex(3)
.transition(.scale.combined(with: .move(edge: .top)))
@ -911,6 +921,7 @@ struct ContentView: View {
historyListViewModel: HistoryListViewModel(),
startCallViewModel: StartCallViewModel(),
callViewModel: CallViewModel(),
meetingWaitingRoomViewModel: MeetingWaitingRoomViewModel(),
conversationsListViewModel: ConversationsListViewModel(),
conversationViewModel: ConversationViewModel()
)

View file

@ -215,11 +215,18 @@ struct HistoryListFragment: View {
if historyListViewModel.callLogs[index].toAddress!.asStringUriOnly().hasPrefix("sip:conference-focus@sip.linphone.org") {
do {
//let reudumatin = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=8~YNkpFOv;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135")
/*
let reutest = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=iVs8XshC~;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135")
telecomManager.doCallWithCore(
addr: reutest, isVideo: false, isConference: true
)
*/
let reutest = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=iVs8XshC~;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135")
telecomManager.meetingWaitingRoomDisplayed = true
telecomManager.meetingWaitingRoomSelected = reutest
} catch {}
} else {
telecomManager.doCallWithCore(
@ -230,10 +237,17 @@ struct HistoryListFragment: View {
if historyListViewModel.callLogs[index].fromAddress!.asStringUriOnly().hasPrefix("sip:conference-focus@sip.linphone.org") {
do {
//let reudumatin = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=8~YNkpFOv;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135")
/*
let reutest = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=iVs8XshC~;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135")
telecomManager.doCallWithCore(
addr: reutest, isVideo: false, isConference: true
)
*/
let reutest = try Factory.Instance.createAddress(addr: "sip:conference-focus@sip.linphone.org;conf-id=iVs8XshC~;gr=0ee3f37f-6df2-0071-bb9a-a4e24be30135")
telecomManager.meetingWaitingRoomDisplayed = true
telecomManager.meetingWaitingRoomSelected = reutest
} catch {}
} else {
telecomManager.doCallWithCore(

View file

@ -23,15 +23,14 @@ struct ActivityIndicator: View {
let style = StrokeStyle(lineWidth: 3, lineCap: .round)
@State var animate = false
let color1 = Color.white
let color2 = Color.white.opacity(0.5)
let color: Color
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [color1, color2]), center: .center), style: style)
AngularGradient(gradient: .init(colors: [color, color.opacity(0.5)]), center: .center), style: style)
.rotationEffect(Angle(degrees: animate ? 360: 0))
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false), value: UUID())
}.onAppear {
@ -41,5 +40,5 @@ struct ActivityIndicator: View {
}
#Preview {
ActivityIndicator()
ActivityIndicator(color: .white)
}