Manual Video Quality Selection

By default, our SDK chooses the incoming video quality that best matches the size of a video element for a given participant. It makes less sense to waste bandwidth receiving Full HD video when it’s going to be displayed in a 320 by 240 pixel rectangle.

However, it’s still possible to override this behavior and manually request higher resolution video for better quality, or lower resolution to save bandwidth. It’s also possible to disable incoming video altogether for an audio-only experience.

Actual incoming video quality depends on a number of factors, such as the quality of the source video, and network conditions. Manual video quality selection allows you to specify your preference, while the actual resolution is automatically selected from the available resolutions to match that preference as closely as possible.

In this article we’ll build a UI control for manual video quality selection.

Getting and Setting Incoming Video Settings

​ To get the current incoming video quality settings, we will use the setIncomingVideoQualitySettings method on Call to set it and the Call.state.incomingVideoQualitySettings to access it. The value is of type incomingVideoQualitySettings which is an enum with the following case:

.none

As the name suggests, it means that we have no preference on the incoming video quality settings. In that way we allow the SDK to take control and decide the best possible settings.

.manual(group: Group, targetSize: CGSize)

Allows manual control over the enabled video streams and based on the group applies the provided targetSize for the video, if required.

.disabled(group: Group)

Disables video streams for the specified group of session IDs.

The Group type is another enum that is defined with the following cases:

Group.all

This group evaluates as true for every given participant.

Group.custom(sessionIds: Set<String>)

While this group evalues as true only for the participants whose sessionIds are contained from the provided set.

With that in mind we can do the following:

// Set all incoming videos to a low resolution
await call.setIncomingVideoQualitySettings(
    .manual(
        group: .all, 
        targetSize: CGSize(width: 640, height: 480)
    )
)

/// Set incoming videos of specific users to a low resolution
// let otherParticipant: CallParticipant = ...
await call.setIncomingVideoQualitySettings(
    .manual(
        group: .custom(sessionIds: [otherParticipant.sessionId]), 
        targetSize: CGSize(width: 640, height: 480)
    )
)

// Disable incoming video for all participants
await call.setIncomingVideoQualitySettings(.disabled(group: .all))

// Disable incoming video for one specific participant
// let otherParticipant: CallParticipant = ...
await call.setIncomingVideoQualitySettings(.disabled(group: .custom(sessionIds: [otherParticipant.sessionId])))

Even though IncomingVideoQualitySettings provides a lot of flexibility, in this cookbook we assume that the preferences apply to all call participants.

Building Incoming Video Quality Selector

Now we’re ready to build a UI control to display and change the incoming video quality.

import Foundation
import StreamVideo
import StreamVideoSwiftUI
import SwiftUI

struct IncomingVideoQualitySelector: View {

    enum ManualQuality: Hashable {
        case auto
        case fourK
        case fullHD
        case HD
        case SD
        case dataSaver
        case disabled

        var policy: IncomingVideoQualitySettings {
            switch self {
            case .auto:
                return .none
            case .fourK:
                return .manual(group: .all, targetSize: .init(width: 3840, height: 2160))
            case .fullHD:
                return .manual(group: .all, targetSize: .init(width: 1920, height: 1080))
            case .HD:
                return .manual(group: .all, targetSize: .init(width: 1280, height: 720))
            case .SD:
                return .manual(group: .all, targetSize: .init(width: 640, height: 480))
            case .dataSaver:
                return .manual(group: .all, targetSize: .init(width: 256, height: 144))
            case .disabled:
                return .disabled(group: .all)
            }
        }
    }

    @State private var isActive: Bool = false

    var call: Call?

    init(call: Call?) {
        self.call = call
    }

    var body: some View {
        Menu {
            buttonView(for: .auto)
            buttonView(for: .fourK)
            buttonView(for: .fullHD)
            buttonView(for: .HD)
            buttonView(for: .SD)
            buttonView(for: .dataSaver)
            buttonView(for: .disabled)
        } label: {
            Button { 
                isActive.toggle() 
            } label: {
                Label {
                    Text("Manual quality")
                } icon: {
                    Image(systemName: "square.resize")
                }
            }
        }
    }

    @MainActor
    @ViewBuilder
    private func buttonView(
        for manualQuality: ManualQuality
    ) -> some View {
        let title = {
            switch manualQuality {
            case .auto:
                return "Auto quality"
            case .fourK:
                return "4K 2160p"
            case .fullHD:
                return "Full HD 1080p"
            case .HD:
                return "HD 720p"
            case .SD:
                return "SD 480p"
            case .dataSaver:
                return "Data saver 144p"
            case .disabled:
                return "Disable video"
            }
        }()
        Button {
            execute(manualQuality)
        } label: {
            Label {
                Text(title)
            } icon: {
                if manualQuality.policy == call?.state.incomingVideoQualitySettings {
                    Image(systemName: "checkmark")
                }
            }
        }
    }

    private func execute(
        _ manualQuality: ManualQuality
    ) {
        Task { @MainActor in
            await call?.setIncomingVideoQualitySettings(manualQuality.policy)
        }
    }
}

Screenshot shows the newly created Manual Quality Selector component.

© Getstream.io, Inc. All Rights Reserved.