Skip to content
Platform docs
Auth, users, webhooks & more
Info:
This is beta documentation for Stream Video React Native SDK v2. For the latest stable version, see the latest version (v1).

Manual Video Quality Selection

The SDK automatically selects incoming video quality based on display size, avoiding bandwidth waste (e.g., receiving Full HD for a 320x240 display).

Override this behavior to manually request higher resolution for quality, lower resolution for bandwidth savings, or disable video for audio-only experience.

Best Practices

  • Default to auto - Let the SDK optimize quality unless users need control
  • Save preferences - Remember user's quality selection across sessions
  • Show current setting - Display the active quality level clearly
  • Explain tradeoffs - Help users understand bandwidth vs quality implications
Note:

Actual quality depends on source video quality and network conditions. Manual selection specifies preference; actual resolution is selected from available options to match as closely as possible.

This guide covers building a UI control for manual video quality selection.

Prerequisites

For a video calling application starting point, see the Video Calling Tutorial.

Basic application setup:

import React, { useEffect } from "react";
import { SafeAreaView, StyleSheet, Text } from "react-native";
import {
  Call,
  StreamCall,
  CallContent,
  CallingState,
  StreamVideoClient,
  StreamVideo,
} from "@stream-io/video-react-native-sdk";

const apiKey = "REPLACE_WITH_API_KEY";
const token = "REPLACE_WITH_TOKEN";
const userId = "REPLACE_WITH_USER_ID";
const callId = "REPLACE_WITH_CALL_ID";

const user = {
  id: userId,
  name: "John Malkovich",
  image: "https://robohash.org/John",
};

const client = new StreamVideoClient({ apiKey, user, token });

const RootContainer = (props: React.PropsWithChildren<{}>) => {
  return <SafeAreaView style={styles.container}>{props.children}</SafeAreaView>;
};

export const App = () => {
  const [call, setCall] = React.useState<Call>();

  useEffect(() => {
    const _call = client.call("default", callId);
    _call.join({ create: true }).then(() => setCall(_call));

    return () => {
      _call.leave().catch(() => console.error("Failed to leave the call"));
      setCall(undefined);
    };
  }, []);

  if (!call) {
    return (
      <RootContainer>
        <Text style={styles.text}>Joining call...</Text>
      </RootContainer>
    );
  }

  return (
    <StreamVideo client={client}>
      <StreamCall call={call}>
        <RootContainer>
          <CallContent />
        </RootContainer>
      </StreamCall>
    </StreamVideo>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    backgroundColor: "white",
  },
  text: {
    color: "black",
    fontSize: 20,
    fontWeight: "bold",
    marginBottom: 20,
    textAlign: "center",
  },
});

export default App;

Getting and Setting Incoming Video Settings

Use the useIncomingVideoSettings hook to get current settings:

  • enabled - Boolean indicating whether incoming video is enabled
  • preferredResolution - Object { width: number; height: number } containing preferred resolution (when video is enabled)

Modify settings using Call object methods:

  • setIncomingVideoEnabled - Enable/disable incoming video, clearing preferred resolution
  • setPreferredIncomingVideoResolution - Set preferred resolution, enabling video if previously disabled

Combine settings into a single control with mapping:

import type { Call } from "@stream-io/video-react-native-sdk";

const incomingVideoSettings = ["auto", "1080p", "720p", "480p", "off"] as const;
type IncomingVideoSetting = (typeof incomingVideoSettings)[number];
type VideoDimension = { width: number; height: number };

const applyIncomingVideoSetting = (
  call: Call,
  setting: IncomingVideoSetting,
) => {
  if (setting === "auto") {
    call.setIncomingVideoEnabled(true);
  } else if (setting === "off") {
    call.setIncomingVideoEnabled(false);
  } else {
    call.setPreferredIncomingVideoResolution(
      incomingVideoSettingToResolution(setting),
    );
  }
};

const incomingVideoSettingToResolution = (
  setting: Exclude<IncomingVideoSetting, "auto" | "off">,
): VideoDimension => {
  switch (setting) {
    case "1080p":
      return { width: 1920, height: 1080 };
    case "720p":
      return { width: 1280, height: 720 };
    case "480p":
      return { width: 640, height: 480 };
  }
};

const incomingVideoResolutionToSetting = (
  resolution: VideoDimension,
): IncomingVideoSetting => {
  switch (true) {
    case resolution.height >= 1080:
      return "1080p";
    case resolution.height >= 720:
      return "720p";
    case resolution.height >= 480:
      return "480p";
    default:
      return "auto";
  }
};

Set preferred resolution per participant using the optional second parameter of setPreferredIncomingVideoResolution with an array of session IDs:

import { useCallStateHooks } from "@stream-io/video-react-native-sdk";

const { useParticipants } = useCallStateHooks();
const participants = useParticipants();
const [dominantParticipant] = participants;

call.setPreferredIncomingVideoResolution(
  incomingVideoSettingToResolution("1080p"),
  [dominantParticipant.sessionId],
);

This guide applies preferred resolution to all call participants.

Building Incoming Video Quality Selector Button

Build a UI control using React Native's Modal component:

IncomingVideoQualitySelectorButton.tsx
import {
  useCall,
  useCallStateHooks,
  type Call,
} from "@stream-io/video-react-native-sdk";
import React, { useState } from "react";
import { Button, Modal, Pressable, StyleSheet, Text, View } from "react-native";

/* This is the individual item inside the model */
const SettingSelectionItem = ({
  value,
  setValue,
  selectedValue,
  closeModal,
}: {
  value: IncomingVideoSetting;
  setValue: (setting: IncomingVideoSetting) => void;
  selectedValue: IncomingVideoSetting;
  closeModal: () => void;
}) => {
  return (
    <Pressable
      onPress={() => {
        setValue(value);
        closeModal();
      }}
      style={styles.modalButton}
    >
      <Text
        style={[
          styles.modalText,
          selectedValue === value ? styles.selectedModalText : null,
        ]}
      >
        {value}
      </Text>
    </Pressable>
  );
};

/* This button that opens the modal along with the modal component */
export const IncomingVideoQualitySelectorButton = () => {
  const [modalVisible, setModalVisible] = useState(false);
  const closeModal = () => setModalVisible(false);

  const call = useCall();
  const { useIncomingVideoSettings } = useCallStateHooks();
  const { enabled, preferredResolution } = useIncomingVideoSettings();
  let currentSetting: IncomingVideoSetting;

  if (!preferredResolution) {
    currentSetting = enabled ? "auto" : "off";
  } else {
    currentSetting = incomingVideoResolutionToSetting(preferredResolution);
  }

  const handleChange = (setting: IncomingVideoSetting) => {
    if (call) {
      applyIncomingVideoSetting(call, setting);
    }
  };

  return (
    <>
      <Modal
        animationType="fade"
        transparent
        visible={modalVisible}
        onRequestClose={closeModal}
      >
        <Pressable
          style={styles.centeredView}
          onPress={() => setModalVisible(false)}
        >
          <View style={styles.modalView} onStartShouldSetResponder={() => true}>
            {incomingVideoSettings.map((setting) => (
              <SettingSelectionItem
                key={setting}
                value={setting}
                selectedValue={currentSetting}
                setValue={handleChange}
                closeModal={closeModal}
              />
            ))}
          </View>
        </Pressable>
      </Modal>
      {/* This is the button to open/close the modal */}
      <Button
        title="Change Incoming Video Quality"
        onPress={() => setModalVisible(true)}
      />
    </>
  );
};

const styles = StyleSheet.create({
  centeredView: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "rgba(0, 0, 0, 0.5)",
  },
  modalView: {
    backgroundColor: "#272A30",
    borderRadius: 20,
    padding: 16,
    alignItems: "flex-start",
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5,
    minWidth: "70%",
  },
  modalButton: {
    padding: 16,
  },
  modalText: {
    fontSize: 20,
    fontWeight: "bold",
    color: "white",
  },
  selectedModalText: {
    color: "blue",
  },
});
Zoom in on the video quality selector modal

Add the component inside StreamCall for a video quality selector button:

<StreamVideo client={client}>
  <StreamCall call={call}>
    <IncomingVideoQualitySelectorButton />
    <SpeakerLayout />
  </StreamCall>
</StreamVideo>
Video quality selector button component in use