From 0c7791b414e655b90da38913ac6aa9f50af5f8a1 Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Fri, 14 Nov 2025 17:47:19 +0100 Subject: [PATCH 1/2] Implemented ring members in call view model --- .../Sources/Components/AppEnvironment.swift | 2 +- .../CallingView/DetailedCallingView.swift | 17 ++++++- Sources/StreamVideo/Call.swift | 10 ++++ .../StreamVideoSwiftUI/CallViewModel.swift | 47 ++++++++++++++++++- 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/DemoApp/Sources/Components/AppEnvironment.swift b/DemoApp/Sources/Components/AppEnvironment.swift index ea66f24eb..27beb85e7 100644 --- a/DemoApp/Sources/Components/AppEnvironment.swift +++ b/DemoApp/Sources/Components/AppEnvironment.swift @@ -212,7 +212,7 @@ extension AppEnvironment { #if targetEnvironment(simulator) return .simple #else - return .simple + return .detailed #endif case .release: return .simple diff --git a/DemoApp/Sources/Views/CallView/CallingView/DetailedCallingView.swift b/DemoApp/Sources/Views/CallView/CallingView/DetailedCallingView.swift index b333f33d3..0e295b29d 100644 --- a/DemoApp/Sources/Views/CallView/CallingView/DetailedCallingView.swift +++ b/DemoApp/Sources/Views/CallView/CallingView/DetailedCallingView.swift @@ -164,7 +164,7 @@ struct DetailedCallingView: View { callType: callType, callId: text, members: members, - ring: callFlow == .ringEvents, + ring: false,//callFlow == .ringEvents, video: viewModel.callSettings.videoOn ) } @@ -189,6 +189,18 @@ struct DetailedCallingView: View { self.callAction = currentUser?.type == .regular ? callAction : .joinCall self.callFlow = currentUser?.type == .regular ? callFlow : .joinImmediately } + .onChange(of: viewModel.callingState) { state in + if state == .inCall && !hasRang { + hasRang = true + viewModel.ring( + callType: callType, + callId: text, + members: members, + video: true, + showOutgoingScreen: true + ) + } + } } @ViewBuilder @@ -244,3 +256,6 @@ struct DetailedCallingView: View { ) } } + +//TODO: temp for testing. +var hasRang = false diff --git a/Sources/StreamVideo/Call.swift b/Sources/StreamVideo/Call.swift index a2763ea43..ef79f775e 100644 --- a/Sources/StreamVideo/Call.swift +++ b/Sources/StreamVideo/Call.swift @@ -362,6 +362,16 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { } return response.call } + + @discardableResult + public func ring(request: RingCallRequest) async throws -> RingCallResponse { + let response = try await coordinatorClient.ringCall( + type: callType, + id: callId, + ringCallRequest: request + ) + return response + } /// Updates an existing call with the specified parameters. /// - Parameters: diff --git a/Sources/StreamVideoSwiftUI/CallViewModel.swift b/Sources/StreamVideoSwiftUI/CallViewModel.swift index 03da5f698..0d68cdd7f 100644 --- a/Sources/StreamVideoSwiftUI/CallViewModel.swift +++ b/Sources/StreamVideoSwiftUI/CallViewModel.swift @@ -193,6 +193,7 @@ open class CallViewModel: ObservableObject { private(set) var localCallSettingsChange = false private var hasAcceptedCall = false + private var skipCallStateUpdates = false public var participants: [CallParticipant] { let updateParticipants = call?.state.participants ?? [] @@ -431,6 +432,46 @@ open class CallViewModel: ObservableObject { customData: customData ) } + + public func ring( + callType: String, + callId: String, + members: [Member], + video: Bool? = nil, + showOutgoingScreen: Bool = false + ) { + outgoingCallMembers = members + if showOutgoingScreen { + skipCallStateUpdates = true + setCallingState(.outgoing) + } + if self.call == nil || (call?.id != callId && call?.callType != callType) { + let callSettings = localCallSettingsChange ? callSettings : nil + let call = streamVideo.call( + callType: callType, + callId: callId, + callSettings: callSettings + ) + self.call = call + } + guard let call else { return } + Task(disposableBag: disposableBag, priority: .userInitiated) { [weak self] in + guard let self else { return } + do { + try await call.ring( + request: .init(membersIds: members.map(\.id).filter { $0 != self.streamVideo.user.id }, video: video) + ) + if let autoCancelTimeout = call.state.settings?.ring.autoCancelTimeoutMs { + let timeoutSeconds = TimeInterval(autoCancelTimeout / 1000) + startTimer(timeout: timeoutSeconds) + } + } catch { + self.error = error + setCallingState(.idle) + self.call = nil + } + } + } /// Enters into a lobby before joining a call. /// - Parameters: @@ -593,7 +634,9 @@ open class CallViewModel: ObservableObject { lineNumber: line ) if let call, (callingState != .inCall || self.call?.cId != call.cId) { - setCallingState(.inCall) + if !skipCallStateUpdates { + setCallingState(.inCall) + } self.call = call } else if call == nil, callingState != .idle { setCallingState(.idle) @@ -887,6 +930,7 @@ open class CallViewModel: ObservableObject { setActiveCall(call) } case .outgoing where call?.cId == event.callCid: + skipCallStateUpdates = false enterCall( call: call, callType: event.type, @@ -942,6 +986,7 @@ open class CallViewModel: ObservableObject { } private func updateCallStateIfNeeded() { + guard !skipCallStateUpdates else { return } if callingState == .outgoing { if !callParticipants.isEmpty { setCallingState(.inCall) From 6ea2561797218345454aa3ffae8cbff37b22b593 Mon Sep 17 00:00:00 2001 From: Martin Mitrevski Date: Sun, 16 Nov 2025 22:24:46 +0100 Subject: [PATCH 2/2] Introduced new flow --- .../Sources/Components/AppEnvironment.swift | 2 +- .../CallingView/DetailedCallingView.swift | 18 +---- .../StreamVideoSwiftUI/CallViewModel.swift | 76 +++++++++++++++++++ 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/DemoApp/Sources/Components/AppEnvironment.swift b/DemoApp/Sources/Components/AppEnvironment.swift index 27beb85e7..467fc8f2b 100644 --- a/DemoApp/Sources/Components/AppEnvironment.swift +++ b/DemoApp/Sources/Components/AppEnvironment.swift @@ -210,7 +210,7 @@ extension AppEnvironment { return .detailed case .debug: #if targetEnvironment(simulator) - return .simple + return .detailed #else return .detailed #endif diff --git a/DemoApp/Sources/Views/CallView/CallingView/DetailedCallingView.swift b/DemoApp/Sources/Views/CallView/CallingView/DetailedCallingView.swift index 0e295b29d..d03638dec 100644 --- a/DemoApp/Sources/Views/CallView/CallingView/DetailedCallingView.swift +++ b/DemoApp/Sources/Views/CallView/CallingView/DetailedCallingView.swift @@ -160,11 +160,10 @@ struct DetailedCallingView: View { members: members ) } else { - viewModel.startCall( + viewModel.joinAndRingCall( callType: callType, callId: text, members: members, - ring: false,//callFlow == .ringEvents, video: viewModel.callSettings.videoOn ) } @@ -189,18 +188,6 @@ struct DetailedCallingView: View { self.callAction = currentUser?.type == .regular ? callAction : .joinCall self.callFlow = currentUser?.type == .regular ? callFlow : .joinImmediately } - .onChange(of: viewModel.callingState) { state in - if state == .inCall && !hasRang { - hasRang = true - viewModel.ring( - callType: callType, - callId: text, - members: members, - video: true, - showOutgoingScreen: true - ) - } - } } @ViewBuilder @@ -256,6 +243,3 @@ struct DetailedCallingView: View { ) } } - -//TODO: temp for testing. -var hasRang = false diff --git a/Sources/StreamVideoSwiftUI/CallViewModel.swift b/Sources/StreamVideoSwiftUI/CallViewModel.swift index 0d68cdd7f..892fe2d02 100644 --- a/Sources/StreamVideoSwiftUI/CallViewModel.swift +++ b/Sources/StreamVideoSwiftUI/CallViewModel.swift @@ -472,7 +472,83 @@ open class CallViewModel: ObservableObject { } } } + + public func joinAndRingCall( + callType: String, + callId: String, + members: [Member], + team: String? = nil, + maxDuration: Int? = nil, + maxParticipants: Int? = nil, + startsAt: Date? = nil, + customData: [String: RawJSON]? = nil, + video: Bool? = nil + ) { + outgoingCallMembers = members + skipCallStateUpdates = true + setCallingState(.outgoing) + let membersRequest: [MemberRequest]? = members.isEmpty + ? nil + : members.map(\.toMemberRequest) + + if enteringCallTask != nil || callingState == .inCall { + return + } + enteringCallTask = Task(disposableBag: disposableBag, priority: .userInitiated) { [weak self] in + guard let self else { return } + do { + log.debug("Starting call") + let call = call ?? streamVideo.call( + callType: callType, + callId: callId, + callSettings: callSettings + ) + var settingsRequest: CallSettingsRequest? + var limits: LimitsSettingsRequest? + if maxDuration != nil || maxParticipants != nil { + limits = .init(maxDurationSeconds: maxDuration, maxParticipants: maxParticipants) + } + settingsRequest = .init(limits: limits) + let options = CreateCallOptions( + members: membersRequest, + custom: customData, + settings: settingsRequest, + startsAt: startsAt, + team: team + ) + let settings = localCallSettingsChange ? callSettings : nil + call.updateParticipantsSorting(with: participantsSortComparators) + + try await call.join( + create: true, + options: options, + ring: false, + callSettings: settings + ) + + try await call.ring( + request: .init(membersIds: members.map(\.id).filter { $0 != self.streamVideo.user.id }, video: video) + ) + + if let autoCancelTimeout = call.state.settings?.ring.autoCancelTimeoutMs { + let timeoutSeconds = TimeInterval(autoCancelTimeout / 1000) + startTimer(timeout: timeoutSeconds) + } + save(call: call) + enteringCallTask = nil + hasAcceptedCall = false + } catch { + hasAcceptedCall = false + log.error("Error starting a call", error: error) + self.error = error + setCallingState(.idle) + audioRecorder.stopRecording() + enteringCallTask = nil + } + } + } + /// Enters into a lobby before joining a call. /// - Parameters: /// - callType: the type of the call.