Session Timers

In some cases, you want to be able to limit the duration of a call. StreamVideo supports this use-case by allowing you to specify the duration of a call on creation. Additionally, it provides you a way to extend the duration during the call, if needed.

Low-level client capabilities

First, let’s see how we can create a call that has limited duration.

let call = streamVideo.call(callType: "default", callId: "callId")
try await call.create(members: [], maxDuration: 300)

This code will create a call, which will have a duration of 300 seconds (5 minutes), as soon as the session is started (a participant joined the call).

You can check the start date of a call with the following code:

let startedAt = call.state.session?.startedAt

When the maxDuration of a call is specified, the call session also provides the timerEndsAt value, which provides the date when the call will end. When a call is ended, all the participants are removed from the call.

let timerEndsAt = call.state.session?.timerEndsAt

Extending the call duration

You can also extend the duration of a call, both before or during the call. To do that, you should use the call.update method:

let newDuration = (call.state.settings?.limits.maxDurationSeconds ?? 0) + Int(extendDuration)
try await call.update(settingsOverride: .init(limits: .init(maxDurationSeconds: newDuration)))

When the call duration is extended, the timerEndsAt will be updated to reflect that change.

Example implementation

Let’s see how we can put these methods together in a sample session timer implementation.

In this cookbook, we will show a popup that will notify the user that a call will end soon. It will also allow the creator of the call to extend its duration.

Prerequisite for following along is a working StreamVideo integration and the ability to establish calls. To help with that, check our tutorials and getting started docs.

Session Timer example

Let’s create a new Swift file, and call it SessionTimer. We will put the following contents in it:

@MainActor class SessionTimer: ObservableObject {
    
    @Published var showTimerAlert: Bool = false {
        didSet {
            if showTimerAlert, let timerEndsAt {
                sessionEndCountdown?.invalidate()
                secondsUntilEnd = timerEndsAt.timeIntervalSinceNow
                sessionEndCountdown = Timer.scheduledTimer(
                    withTimeInterval: 1.0,
                    repeats: true,
                    block: { [weak self] _ in
                        guard let self else { return }
                        Task { @MainActor in
                            if self.secondsUntilEnd <= 0 {
                                self.sessionEndCountdown?.invalidate()
                                self.sessionEndCountdown = nil
                                self.secondsUntilEnd = 0
                                self.showTimerAlert = false
                                return
                            }
                            self.secondsUntilEnd -= 1
                        }
                    }
                )
            } else if !showTimerAlert {
                sessionEndCountdown?.invalidate()
                secondsUntilEnd = 0
            }
        }
    }
    
    @Published var secondsUntilEnd: TimeInterval = 0

    private var call: Call?
    private var cancellables = Set<AnyCancellable>()
    private var timerEndsAt: Date? {
        didSet {
            setupTimerIfNeeded()
        }
    }

    private var timer: Timer?
    private var sessionEndCountdown: Timer?
    
    private let alertInterval: TimeInterval
    
    private var extendDuration: TimeInterval
    
    private let changeMaxDurationPermission = Permission(
        rawValue: OwnCapability.changeMaxDuration.rawValue
    )
    
    let extensionTime: TimeInterval
    
    var showExtendCallDurationButton: Bool {
        call?.state.ownCapabilities.contains(.changeMaxDuration) == true
    }

    @MainActor init(
        call: Call?,
        alertInterval: TimeInterval,
        extendDuration: TimeInterval = 120
    ) {
        self.call = call
        self.alertInterval = alertInterval
        self.extendDuration = extendDuration
        extensionTime = extendDuration
        timerEndsAt = call?.state.session?.timerEndsAt
        setupTimerIfNeeded()
        subscribeForSessionUpdates()
    }
    
    func extendCallDuration() {
        guard let call else { return }
        Task {
            do {
                let newDuration = (call.state.settings?.limits.maxDurationSeconds ?? 0) + Int(extendDuration)
                extendDuration += extendDuration
                log.debug("Extending call duration to \(newDuration) seconds")
                try await call.update(settingsOverride: .init(limits: .init(maxDurationSeconds: newDuration)))
                showTimerAlert = false
            } catch {
                log.error("Error extending call duration \(error.localizedDescription)")
            }
        }
    }
    
    // MARK: - private
    
    private func subscribeForSessionUpdates() {
        call?.state.$session.sink { [weak self] response in
            guard let self else { return }
            if response?.timerEndsAt != self.timerEndsAt {
                self.timerEndsAt = response?.timerEndsAt
            }
        }
        .store(in: &cancellables)
    }
    
    private func setupTimerIfNeeded() {
        timer?.invalidate()
        timer = nil
        showTimerAlert = false
        if let timerEndsAt {
            let alertDate = timerEndsAt.addingTimeInterval(-alertInterval)
            let timerInterval = alertDate.timeIntervalSinceNow
            if timerInterval < 0 {
                showTimerAlert = true
                return
            }
            log.debug("Starting a timer in \(timerInterval) seconds")
            timer = Timer.scheduledTimer(
                withTimeInterval: timerInterval,
                repeats: false,
                block: { [weak self] _ in
                    guard let self else { return }
                    log.debug("Showing timer alert")
                    Task { @MainActor in
                        self.showTimerAlert = true
                    }
                }
            )
        }
    }
    
    deinit {
        timer?.invalidate()
        sessionEndCountdown?.invalidate()
    }
}

The session timer will be used in our SwiftUI view shown during a call. It will provide information when the session timer popup should be shown, as well as a countdown timer.

The showTimerAlert published variable will be set to true whenever the popup should be shown. This value is set whenever the timerEndsAt variable is updated. We listen to this value in the subscribeForSessionUpdates method above.

When the popup is shown, we start a timer called sessionEndCountdown, which will count down the seconds until the call is ended. We will use this value in the UI layer to inform the user.

We also created a method called extendCallDuration, which will allows us the extend the duration of the call, which would be an option provided to the user.

Next, let’s declare this timer in our view shown during a duration of the call:

@StateObject var sessionTimer: SessionTimer

init(
    // other params ommited
    call: Call?
) {
    _sessionTimer = .init(wrappedValue: .init(call: call, alertInterval: 60))
}

Our SessionTimer is created with a Call object, and an alertInterval. The alert interval in seconds tells us when the popup for session end should appear. The 60 seconds value means that the popup will be shown a minute before the session expires. Depending on your app’s use-case, you can set a bigger value.

We can set the popup view as an overlay to your existing call view:

YourExistingCallView()
    .overlay(
        sessionTimer.showTimerAlert ? DemoSessionTimerView(sessionTimer: sessionTimer) : nil
    )

View implementation

Next, let’s see a sample implementation of the DemoSessionTimerView:

struct DemoSessionTimerView: View {
    
    @Injected(\.colors) var colors
    @Injected(\.fonts) var fonts
    public var formatter: DateComponentsFormatter = {
        let formatter = DateComponentsFormatter()
        formatter.unitsStyle = .positional
        formatter.allowedUnits = [.hour, .minute, .second]
        formatter.zeroFormattingBehavior = .pad
        return formatter
    }()

    @ObservedObject var sessionTimer: SessionTimer
    
    var body: some View {
        VStack {
            HStack {
                if let duration = formatter.string(from: sessionTimer.secondsUntilEnd) {
                    Text("Call will end in \(duration)")
                        .font(fonts.body.monospacedDigit())
                        .minimumScaleFactor(0.2)
                        .lineLimit(1)
                } else {
                    Text("Call will end soon")
                }
                Divider()
                if sessionTimer.showExtendCallDurationButton {
                    Button(action: {
                        sessionTimer.extendCallDuration()
                    }, label: {
                        Text("Extend for \(Int(sessionTimer.extensionTime / 60)) min")
                            .bold()
                    })
                }
            }
            .foregroundColor(Color(colors.callDurationColor))
            .padding(.horizontal)
            .padding(.vertical, 4)
            .background(Color(colors.participantBackground))
            .clipShape(Capsule())
            .frame(height: 60)
            .padding(.top, 80)
            
            Spacer()
        }
    }
}

In the implementation, we are formatting the secondsUntilEnd value from the sessionTimer, in order to inform the user about the session end.

Additionally, we expose a button for extending the call duration. The visibility of this button is controlled by the showExtendCallDurationButton from the sessionTimer, which checks if the user has the changeMaxDuration capability:

var showExtendCallDurationButton: Bool {
    call?.state.ownCapabilities.contains(.changeMaxDuration) == true
}

With that, you can have a working implementation of a session timer.

© Getstream.io, Inc. All Rights Reserved.