@@ -84,7 +84,24 @@ open class CallViewModel: ObservableObject {
8484
8585 /// Tracks the current state of a call. It should be used to show different UI in your views.
8686 @Published public var callingState : CallingState = . idle {
87- didSet { handleRingingEvents ( ) }
87+ didSet {
88+ // When we join a call and then ring, we need to disable the speaker.
89+ // If the dashboard settings have the speaker on, we need to enable it
90+ // again when we transition into a call.
91+ if let temporaryCallSettings, oldValue == . outgoing && callingState == . inCall {
92+ if temporaryCallSettings. speakerOn {
93+ Task {
94+ do {
95+ try await call? . speaker. enableSpeakerPhone ( )
96+ } catch {
97+ log. error ( " Error enabling the speaker: \( error. localizedDescription) " )
98+ }
99+ }
100+ }
101+ self . temporaryCallSettings = nil
102+ }
103+ handleRingingEvents ( )
104+ }
88105 }
89106
90107 /// Optional, has a value if there was an error. You can use it to display more detailed error messages to the users.
@@ -193,6 +210,8 @@ open class CallViewModel: ObservableObject {
193210 private( set) var localCallSettingsChange = false
194211
195212 private var hasAcceptedCall = false
213+ private var skipCallStateUpdates = false
214+ private var temporaryCallSettings : CallSettings ?
196215
197216 public var participants : [ CallParticipant ] {
198217 let updateParticipants = call? . state. participants ?? [ ]
@@ -431,7 +450,97 @@ open class CallViewModel: ObservableObject {
431450 customData: customData
432451 )
433452 }
453+
454+ /// Joins a call and then rings the specified members.
455+ /// - Parameters:
456+ /// - callType: The type of the call to join (for example, "default").
457+ /// - callId: The unique identifier of the call.
458+ /// - members: The members who should be rung for this call.
459+ /// - team: An optional team identifier to associate with the call.
460+ /// - maxDuration: The maximum duration of the call in seconds.
461+ /// - maxParticipants: The maximum number of participants allowed in the call.
462+ /// - startsAt: An optional scheduled start time for the call.
463+ /// - customData: Optional custom payload to associate with the call on creation.
464+ /// - video: Optional flag indicating whether the ring should suggest a video call.
465+ public func joinAndRingCall(
466+ callType: String ,
467+ callId: String ,
468+ members: [ Member ] ,
469+ team: String ? = nil ,
470+ maxDuration: Int ? = nil ,
471+ maxParticipants: Int ? = nil ,
472+ startsAt: Date ? = nil ,
473+ customData: [ String : RawJSON ] ? = nil ,
474+ video: Bool ? = nil
475+ ) {
476+ outgoingCallMembers = members
477+ skipCallStateUpdates = true
478+ setCallingState ( . outgoing)
479+ let membersRequest : [ MemberRequest ] ? = members. isEmpty
480+ ? nil
481+ : members. map ( \. toMemberRequest)
482+
483+ if enteringCallTask != nil || callingState == . inCall {
484+ return
485+ }
486+ enteringCallTask = Task ( disposableBag: disposableBag, priority: . userInitiated) { [ weak self] in
487+ guard let self else { return }
488+ do {
489+ log. debug ( " Starting call " )
490+ let call = call ?? streamVideo. call (
491+ callType: callType,
492+ callId: callId,
493+ callSettings: callSettings
494+ )
495+ var settingsRequest : CallSettingsRequest ?
496+ var limits : LimitsSettingsRequest ?
497+ if maxDuration != nil || maxParticipants != nil {
498+ limits = . init( maxDurationSeconds: maxDuration, maxParticipants: maxParticipants)
499+ }
500+ settingsRequest = . init( limits: limits)
501+ let options = CreateCallOptions (
502+ members: membersRequest,
503+ custom: customData,
504+ settings: settingsRequest,
505+ startsAt: startsAt,
506+ team: team
507+ )
508+ let settings = localCallSettingsChange ? callSettings : nil
434509
510+ call. updateParticipantsSorting ( with: participantsSortComparators)
511+
512+ let joinResponse = try await call. join (
513+ create: true ,
514+ options: options,
515+ ring: false ,
516+ callSettings: settings
517+ )
518+
519+ temporaryCallSettings = call. state. callSettings
520+ try ? await call. speaker. disableSpeakerPhone ( )
521+
522+ try await call. ring (
523+ request: . init( membersIds: members. map ( \. id) . filter { $0 != self . streamVideo. user. id } , video: video)
524+ )
525+
526+ let autoCancelTimeoutMs = call. state. settings? . ring. autoCancelTimeoutMs
527+ ?? joinResponse. call. settings. ring. autoCancelTimeoutMs
528+ let timeoutSeconds = TimeInterval ( autoCancelTimeoutMs) / 1000
529+ startTimer ( timeout: timeoutSeconds)
530+ save ( call: call)
531+ enteringCallTask = nil
532+ hasAcceptedCall = false
533+ } catch {
534+ hasAcceptedCall = false
535+ log. error ( " Error starting a call " , error: error)
536+ self . error = error
537+ setCallingState ( . idle)
538+ audioRecorder. stopRecording ( )
539+ enteringCallTask = nil
540+ }
541+ }
542+ }
543+
435544 /// Enters into a lobby before joining a call.
436545 /// - Parameters:
437546 /// - callType: the type of the call.
@@ -593,7 +702,9 @@ open class CallViewModel: ObservableObject {
593702 lineNumber: line
594703 )
595704 if let call, ( callingState != . inCall || self . call? . cId != call. cId) {
596- setCallingState ( . inCall)
705+ if !skipCallStateUpdates {
706+ setCallingState ( . inCall)
707+ }
597708 self . call = call
598709 } else if call == nil , callingState != . idle {
599710 setCallingState ( . idle)
@@ -656,6 +767,8 @@ open class CallViewModel: ObservableObject {
656767 screenSharingUpdates = nil
657768 recordingUpdates? . cancel ( )
658769 recordingUpdates = nil
770+ skipCallStateUpdates = false
771+ temporaryCallSettings = nil
659772 call? . leave ( )
660773
661774 pictureInPictureAdapter. call = nil
@@ -769,6 +882,9 @@ open class CallViewModel: ObservableObject {
769882 }
770883
771884 private func handleCallHangUp( ringTimeout: Bool = false ) {
885+ if skipCallStateUpdates {
886+ skipCallStateUpdates = false
887+ }
772888 guard
773889 let call,
774890 callingState == . outgoing
@@ -887,6 +1003,7 @@ open class CallViewModel: ObservableObject {
8871003 setActiveCall ( call)
8881004 }
8891005 case . outgoing where call? . cId == event. callCid:
1006+ skipCallStateUpdates = false
8901007 enterCall (
8911008 call: call,
8921009 callType: event. type,
@@ -929,6 +1046,10 @@ open class CallViewModel: ObservableObject {
9291046 } ( )
9301047 let accepted = outgoingCall. state. session? . acceptedBy. count ?? 0
9311048 if accepted == 0 , rejections >= outgoingMembersCount {
1049+ if skipCallStateUpdates {
1050+ skipCallStateUpdates = false
1051+ setCallingState ( . idle)
1052+ }
9321053 Task ( disposableBag: disposableBag, priority: . userInitiated) { [ weak self] in
9331054 _ = try ? await outgoingCall. reject (
9341055 reason: " Call rejected by all \( outgoingMembersCount) outgoing call members. "
@@ -942,6 +1063,7 @@ open class CallViewModel: ObservableObject {
9421063 }
9431064
9441065 private func updateCallStateIfNeeded( ) {
1066+ guard !skipCallStateUpdates else { return }
9451067 if callingState == . outgoing {
9461068 if !callParticipants. isEmpty {
9471069 setCallingState ( . inCall)
0 commit comments