Managing network disruptions during a call

Summary

This tutorial guides you through using the setDisconnectionTimeout method within the Call object to manage user disconnections due to network issues. By setting a timeout, users are given a grace period to reconnect before they are removed from the call, ensuring that temporary network disruptions don’t immediately end their participation.

Overview

The setDisconnectionTimeout method allows you to specify how long a user can remain disconnected before being removed from the call. This is particularly useful when users experience brief network interruptions but can reconnect quickly. By setting a timeout, you ensure that users are only dropped if their disconnection persists beyond the specified duration.

:::note By default the disconnectionTimeout is set to 0, allowing the user either to remain in the call until their connection restores or select to hang up. :::

Setting Up the Disconnection Timeout

Once the call has been created, you can set a disconnection timeout that defines how long a user can stay disconnected before being dropped. Here’s how to do it:

let call = streamVideo.call(callType: "default", callId: callId)

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

Inform the user after a disconnection occurs

With that set, we want to make sure to inform the user once they get disconnected due to a network disruption. To do that, we are going to extend the FeedbackView we created here. Specifically we are going to update the DemoFeedbackView like below:

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

    @MainActor
    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."
            )
        }
    }
}

The parts that we changed here are:

  • @State private var toast: Toast?We define a state property for the Toast that is going to be presented to the user.
  • .toastView(toast: $toast) We attach the toastView ViewModifier on our view (similar to how we are doing with alert).
  • .onAppear { checkIfDisconnectionErrorIsAvailable() } On appear we are checking if there is an error of type NetworkNotAvailable and if there, we setup a toast to be presented.
  • checkIfDisconnectionErrorIsAvailable() We define a method that will do the error checking for us.

Conclusion

By configuring the setDisconnectionTimeout and handling disconnection errors using the disconnectionError property, you can provide a more seamless experience for users, allowing them a grace period to reconnect during temporary network issues. Additionally, by integrating user feedback mechanisms, you can give users clear notifications when they have been disconnected due to network problems, helping them understand the issue and take appropriate action. This approach enhances both the reliability of your video calls and user satisfaction, even in less-than-ideal network conditions.

© Getstream.io, Inc. All Rights Reserved.