Skip to content

Commit

Permalink
[Feature]Disconnection timeout during call
Browse files Browse the repository at this point in the history
  • Loading branch information
ipavlidakis committed Oct 17, 2024
1 parent b7766ec commit a7967ed
Show file tree
Hide file tree
Showing 20 changed files with 593 additions and 70 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### ✅ Added
- You can now provide the incoming video quality setting for some or all participants [#571](https://github.com/GetStream/stream-video-swift/pull/571)
- You can now set the time a user can remain in the call - after their connection disrupted - while waiting for their network connection to recover [#573](https://github.com/GetStream/stream-video-swift/pull/573)

# [1.13.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.13.0)
_October 08, 2024_
Expand Down
2 changes: 1 addition & 1 deletion DemoApp/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
.current()
.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
if granted {
DispatchQueue.main.async {
Task { @MainActor in
UIApplication.shared.registerForRemoteNotifications()
}
}
Expand Down
14 changes: 14 additions & 0 deletions DemoApp/Sources/Components/Feedback/DemoFeedbackView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ struct DemoFeedbackView: View {
@State private var comment: String = ""
@State private var rating: Int = 5
@State private var isSubmitting = false
@State private var toast: Toast?

private weak var call: Call?
private var dismiss: () -> Void
Expand Down Expand Up @@ -121,6 +122,19 @@ struct DemoFeedbackView: View {
.padding(.horizontal)
}
.withModalNavigationBar(title: "", closeAction: dismiss)
.toastView(toast: $toast)
.onAppear { checkIfDisconnectionErrorIsAvailable() }
}

// MARK: - Private helpers

func checkIfDisconnectionErrorIsAvailable() {
if call?.state.disconnectionError is ClientError.NetworkNotAvailable {
toast = .init(
style: .error,
message: "Your call was ended because it seems your internet connection is down."
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
400D91D12B63DEA200EBA47D /* 04-camera-and-microphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400D91D02B63DEA200EBA47D /* 04-camera-and-microphone.swift */; };
400D91D32B63DFA500EBA47D /* 06-querying-calls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400D91D22B63DFA500EBA47D /* 06-querying-calls.swift */; };
400D91D52B63E27300EBA47D /* 07-dependency-injection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400D91D42B63E27300EBA47D /* 07-dependency-injection.swift */; };
4029E95E2CB94EAE00E1D571 /* 21-manual-quality-selection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4029E95D2CB94EA700E1D571 /* 21-manual-quality-selection.swift */; };
4029E95E2CB94EAE00E1D571 /* 22-manual-quality-selection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4029E95D2CB94EA700E1D571 /* 22-manual-quality-selection.swift */; };
404CAED82B8E3874007087BC /* 06-apply-video-filters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404CAED72B8E3874007087BC /* 06-apply-video-filters.swift */; };
4068C1252B67C056006B0BEE /* 03-callkit-integration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4068C1242B67C056006B0BEE /* 03-callkit-integration.swift */; };
408CE0F52BD91B490052EC3A /* 19-transcriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408CE0F42BD91B490052EC3A /* 19-transcriptions.swift */; };
409774B02CC19F5500E0D3EE /* 23-network-disruption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409774AF2CC19F4900E0D3EE /* 23-network-disruption.swift */; };
409C39692B67CC5C0090044C /* 04-screensharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409C39682B67CC5C0090044C /* 04-screensharing.swift */; };
409C396B2B67CD0B0090044C /* 05-picture-in-picture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409C396A2B67CD0B0090044C /* 05-picture-in-picture.swift */; };
409C396D2B67CD780090044C /* 08-recording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409C396C2B67CD780090044C /* 08-recording.swift */; };
Expand Down Expand Up @@ -94,10 +95,11 @@
400D91D02B63DEA200EBA47D /* 04-camera-and-microphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-camera-and-microphone.swift"; sourceTree = "<group>"; };
400D91D22B63DFA500EBA47D /* 06-querying-calls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "06-querying-calls.swift"; sourceTree = "<group>"; };
400D91D42B63E27300EBA47D /* 07-dependency-injection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "07-dependency-injection.swift"; sourceTree = "<group>"; };
4029E95D2CB94EA700E1D571 /* 21-manual-quality-selection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "21-manual-quality-selection.swift"; sourceTree = "<group>"; };
4029E95D2CB94EA700E1D571 /* 22-manual-quality-selection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "22-manual-quality-selection.swift"; sourceTree = "<group>"; };
404CAED72B8E3874007087BC /* 06-apply-video-filters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "06-apply-video-filters.swift"; sourceTree = "<group>"; };
4068C1242B67C056006B0BEE /* 03-callkit-integration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-callkit-integration.swift"; sourceTree = "<group>"; };
408CE0F42BD91B490052EC3A /* 19-transcriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "19-transcriptions.swift"; sourceTree = "<group>"; };
409774AF2CC19F4900E0D3EE /* 23-network-disruption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "23-network-disruption.swift"; sourceTree = "<group>"; };
409C39682B67CC5C0090044C /* 04-screensharing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-screensharing.swift"; sourceTree = "<group>"; };
409C396A2B67CD0B0090044C /* 05-picture-in-picture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "05-picture-in-picture.swift"; sourceTree = "<group>"; };
409C396C2B67CD780090044C /* 08-recording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "08-recording.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -274,7 +276,8 @@
408CE0F42BD91B490052EC3A /* 19-transcriptions.swift */,
40F18B8F2BEBC97F00ADF76E /* 18-call-quality-rating.swift */,
40F290AC2BDFB3CA00DCF136 /* 20-noise-cancellation.swift */,
4029E95D2CB94EA700E1D571 /* 21-manual-quality-selection.swift */,
4029E95D2CB94EA700E1D571 /* 22-manual-quality-selection.swift */,
409774AF2CC19F4900E0D3EE /* 23-network-disruption.swift */,
);
path = "05-ui-cookbook";
sourceTree = "<group>";
Expand Down Expand Up @@ -465,12 +468,13 @@
4068C1252B67C056006B0BEE /* 03-callkit-integration.swift in Sources */,
40FFDC762B63F7D6004DA7A2 /* ChatGloballyUsedVariables.swift in Sources */,
84BA15AE2CA2EF420018DC51 /* 07-querying-call-members.swift in Sources */,
409774B02CC19F5500E0D3EE /* 23-network-disruption.swift in Sources */,
40FFDC672B63F430004DA7A2 /* 04-connection-quality-indicator.swift in Sources */,
40B468982B67B6DF009B5B3E /* 01-deeplinking.swift in Sources */,
40FFDC3B2B63E493004DA7A2 /* 10-view-slots.swift in Sources */,
40FFDC442B63E95D004DA7A2 /* 14-swiftui-vs-uikit.swift in Sources */,
40FFDC872B63FEAE004DA7A2 /* 05-incoming-call.swift in Sources */,
4029E95E2CB94EAE00E1D571 /* 21-manual-quality-selection.swift in Sources */,
4029E95E2CB94EAE00E1D571 /* 22-manual-quality-selection.swift in Sources */,
40FFDC942B6401CC004DA7A2 /* 07-video-fallback.swift in Sources */,
400D91C72B63D96800EBA47D /* 03-quickstart.swift in Sources */,
40FFDC9E2B64063D004DA7A2 /* 12-connection-unstable.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import StreamVideo
import StreamVideoSwiftUI
import SwiftUI
import Combine

@MainActor
fileprivate func content() {
container {
let call = streamVideo.call(callType: "default", callId: callId)

// Set the disconnection timeout to 60 seconds
call.setDisconnectionTimeout(60)
}

container {
struct DemoFeedbackView: View {

@Environment(\.openURL) private var openURL
@Injected(\.appearance) private var appearance

@State private var email: String = ""
@State private var comment: String = ""
@State private var rating: Int = 5
@State private var isSubmitting = false
@State private var toast: Toast?

private weak var call: Call?
private var dismiss: () -> Void
private var isSubmitEnabled: Bool { !email.isEmpty && !isSubmitting }

init(_ call: Call, dismiss: @escaping () -> Void) {
self.call = call
self.dismiss = dismiss
}

var body: some View {
ScrollView {
VStack(spacing: 32) {
Image("feedbackLogo")

VStack(spacing: 8) {
Text("How is your call going?")
.font(appearance.fonts.headline)
.foregroundColor(appearance.colors.text)
.lineLimit(1)

Text("All feedback is celebrated!")
.font(appearance.fonts.subheadline)
.foregroundColor(.init(appearance.colors.textLowEmphasis))
.lineLimit(2)
}
.frame(maxWidth: .infinity, alignment: .center)
.multilineTextAlignment(.center)

VStack(spacing: 27) {
VStack(spacing: 16) {
TextField(
"Email Address *",
text: $email
)
.textFieldStyle(DemoTextfieldStyle())

DemoTextEditor(text: $comment, placeholder: "Message")
}

HStack {
Text("Rate Quality")
.font(appearance.fonts.body)
.foregroundColor(.init(appearance.colors.textLowEmphasis))
.frame(maxWidth: .infinity, alignment: .leading)

DemoStarRatingView(rating: $rating)
}
}

HStack {
Button {
resignFirstResponder()
openURL(.init(string: "https://getstream.io/video/#contact")!)
} label: {
Text("Contact Us")
}
.frame(maxWidth: .infinity)
.foregroundColor(appearance.colors.text)
.padding(.vertical, 4)
.clipShape(Capsule())
.overlay(Capsule().stroke(Color(appearance.colors.textLowEmphasis), lineWidth: 1))

Button {
resignFirstResponder()
isSubmitting = true
Task {
do {
try await call?.collectUserFeedback(
rating: rating,
reason: """
\(email)
\(comment)
"""
)
Task { @MainActor in
dismiss()
}
isSubmitting = false
} catch {
log.error(error)
dismiss()
isSubmitting = false
}
}
} label: {
if isSubmitting {
ProgressView()
} else {
Text("Submit")
}
}
.frame(maxWidth: .infinity)
.foregroundColor(appearance.colors.text)
.padding(.vertical, 4)
.background(isSubmitEnabled ? appearance.colors.accentBlue : appearance.colors.lightGray)
.disabled(!isSubmitEnabled)
.clipShape(Capsule())
}

Spacer()
}
.padding(.horizontal)
}
.toastView(toast: $toast)
.onAppear { checkIfDisconnectionErrorIsAvailable() }
}

// MARK: - Private helpers

func checkIfDisconnectionErrorIsAvailable() {
if call?.state.disconnectionError is ClientError.NetworkNotAvailable {
toast = .init(
style: .error,
message: "Your call was ended because it seems your internet connection is down."
)
}
}
}
}
}
27 changes: 27 additions & 0 deletions Sources/StreamVideo/Call.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,29 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
await callController.setIncomingVideoQualitySettings(value)
}

/// Sets the disconnection timeout for a user who has temporarily lost connection.
///
/// This method defines the duration a user, who has already joined the call, can remain
/// in a disconnected state due to temporary internet issues. If the user’s connection
/// remains disrupted beyond the specified timeout period, they will be dropped from the call.
/// This timeout helps ensure that users with unstable connections do not stay in the call
/// indefinitely if they cannot reconnect.
///
/// - Parameters:
/// - timeout: The time interval, in seconds, that specifies how long a user can stay
/// disconnected before being removed from the call. For example, if the
/// timeout is set to 60 seconds, the user will be dropped from the call if
/// they do not reconnect within that timeframe. Defaults to 0, where the user will
/// remain in the disconnected state until their connection restores or the user hangs
/// up manually.
///
/// - Important: This mechanism is critical in managing users with unstable internet
/// connections, ensuring that temporary network issues are handled gracefully
/// but long-term disconnections result in the user being removed from the call.
public func setDisconnectionTimeout(_ timeout: TimeInterval) {
callController.setDisconnectionTimeout(timeout)
}

// MARK: - Internal

internal func update(reconnectionStatus: ReconnectionStatus) {
Expand Down Expand Up @@ -1190,8 +1213,12 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
}
}

@MainActor
func transitionDueToError(_ error: Error) {
do {
if stateMachine.currentStage.id == .joined {
state.disconnectionError = error
}
try stateMachine.transition(.error(self, error: error))
} catch {
log.error(error)
Expand Down
9 changes: 9 additions & 0 deletions Sources/StreamVideo/CallState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ public class CallState: ObservableObject {
/// disabled, each potentially applying to specific session IDs.
@Published public internal(set) var incomingVideoQualitySettings: IncomingVideoQualitySettings = .none

/// This property holds the error that indicates the user has been disconnected
/// due to a network-related issue. When the user’s connection is disrupted for longer than the specified
/// timeout, this error will be set with a relevant error type, such as
/// `ClientError.NetworkNotAvailable`.
///
/// - SeeAlso: ``ClientError.NetworkNotAvailable`` for the type of error set when a
/// disconnection due to network issues occurs.
@Published public internal(set) var disconnectionError: Error?

var sortComparators = defaultComparators

private var localCallSettingsUpdate = false
Expand Down
12 changes: 9 additions & 3 deletions Sources/StreamVideo/Controllers/CallController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,10 @@ class CallController: @unchecked Sendable {
await webRTCCoordinator.setIncomingVideoQualitySettings(value)
}

func setDisconnectionTimeout(_ timeout: TimeInterval) {
webRTCCoordinator.setDisconnectionTimeout(timeout)
}

// MARK: - private

private func handleParticipantsUpdated() {
Expand Down Expand Up @@ -551,10 +555,12 @@ class CallController: @unchecked Sendable {

call?.update(reconnectionStatus: .connected)
case .error:
if let call, let errorStage = stage as? WebRTCCoordinator.StateMachine.Stage.ErrorStage {
call.transitionDueToError(errorStage.error)
Task { @MainActor in
if let call, let errorStage = stage as? WebRTCCoordinator.StateMachine.Stage.ErrorStage {
call.transitionDueToError(errorStage.error)
}
call?.leave()
}
call?.leave()
default:
break
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/StreamVideo/Errors/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ extension ClientError {

/// Networking error.
public class NetworkError: ClientError {}


/// Represents a network-related error indicating that the network is unavailable.
public class NetworkNotAvailable: ClientError {}

/// Permissions error.
public class MissingPermissions: ClientError {}

Expand Down
Loading

0 comments on commit a7967ed

Please sign in to comment.