Blog
Deep links

How to use react-native-video in Fire TV app

Learn how to build a video streaming app for Fire TV using the react-native-video library. This step-by-step guide will take you through the basics of using react-native-video to boost your Fire TV app development, making sure your app stands out in the Amazon ecosystem
Tags
React Native
React Native Video
tvOS
Fire TV
By
Kamil Moskała
July 24, 2024
5 minute read

How to use react-native-video in Fire TV app

Introduction

Fire OS is Amazon's customized Android-based operating system that powers Fire TV, bringing tons of streaming options to millions of users. With react-native-video, React Native developers can create rich, interactive video apps that fit perfectly into the Fire OS ecosystem. This guide will take you through the basics of using react-native-video to boost your Fire TV app development, making sure your app stands out in the Amazon ecosystem. If you run into any issues while working on your app, feel free to join our Discord for support.

Installation

npx react-native-tvos@latest init AppName
How it works on Apple TV?

After executing the command, we can see in our `package.json` file an additional script to run our app on the Apple TV simulator:

"scripts": {
	// ...  
	"tvos": "expo run:ios --scheme AppName-tvOS --device \"Apple TV\"",  
}

Before we run this script, we need to remember to download the simulator inside the Xcode app.

How it works on Fire TV?

If we want to run the app on a physical Fire TV Stick device, we should make the following steps:

Enable Developer Options on Fire TV Stick
- On your Fire TV Stick, go to Settings > My Fire TV > About
- Click several times on the button with your TV Stick device name. You should see a notification saying "You are now a developer!"

Enable ADB Debugging
- Go to Settings > My Fire TV > Developer Options
- Turn on ADB Debugging
- If you see the option Apps from Unknown Sources, turn it on

Connect Fire TV Stick to Your Computer
- Make sure your Fire TV Stick and your computer are connected to the same Wi-Fi network
- Find the IP address of your Fire TV Stick. Go to Settings > My Fire TV > About > Network and note the IP address.
- On your computer, open a terminal and run the following command to connect to your Fire TV Stick:

adb connect <your-fire-tv-ip-address>:5555

e.g.

adb connect 192.168.0.101:5555

A prompt may appear on your Fire TV Stick asking if you allow the connection. Confirm the prompt and run the command again.

To run your app on this device run the following:

adb -s  <device name> reverse tcp:8081 tcp:8081

you can find device name with `adb devices` command

and later

yarn android

after running the command above, the app will run on the device

Ensuring Good Performance on Both Fire TV and Apple TV

When developing a TV app using React Native, it's important to ensure that the app works well on both Fire TV and Apple TV. While button focusing on Fire TV ans Android TV generally works well out of the box (though sometimes it requires minor adjustments with properties like `nextFocusUp`, `nextFocusRight`, etc.), it doesn't always work correctly on iOS. This can lead to a poor user experience on Apple TV. Fortunately, there are ways to address this issue and improve button focusing on Apple TV.
According to the React Native documentation, `Touchable` components are auto-focused. And yes, that's true, but on Apple devices only when these elements are arranged next to each other along the x or y axis. For example, if we have a situation like this:

When button 1.3 is focused, after clicking the "down" button on the remote, the element will focus on 3.3, not 2.2. To fix this issue, we can use the `TVFocusGuideView` component, which provides support for Apple’s UIFocusGuide API.

When we wrap our boxes with the TVFocusGuideView component and set the autoFocus flag, everything looks great.

the code for this example should look like this

import { View, TVFocusGuideView } from "react-native";
// ...
<>
  <View>
    <Box>1.1</Box>
    <Box>1.2</Box>
    {/* ... */}
  </View>
  <TVFocusGuideView autoFocus>
    <View>
      <Box>2.1</Box>
      {/* ... */}
    </View>
  </TVFocusGuideView>
  <View>
    <Box>3.1</Box>
    {/* ... */}
  </View>
</>;


TVFocusGuideView acts as an extension of the view, allowing it to be aligned with other elements along an axis and to be focusable.

Next example is much more important, because when we see the situation like this, we won't be able to focus on the button 2.1 from the button 1.2 because the button 2.1 is not aligned with the button 1.2 or 3.1 along the x or y axis.

The best idea is to wrap all sections with the TVFocusGuideView component and set the autoFocus property. This way, we can navigate between all buttons seamlessly. TVFocusGuide also works on Android TV and Fire TV because it remembers the last selected item in a for example FlatList, and after navigating away and returning, this last selected item is focused. Look here.

Encountering issues while building Fire TV or Apple TV apps in React Native?

If you need assistance, let’s talk. As core contributors to React Native Video, we can provide you with a free ballpark quote for building your app.

Let's start coding

After creating the app, we should dive into how to build a streaming application on tvOS. We will use the `react-native-video` library, which enables us to create a seamless video playback. It provide playback control, support for various video formats or sources and much more!
First, we will outline our app. We want to create a functional tool for browsing videos. We will separate the views into two screens, the first with a list of movies and the second with a preview of the selected one. To do this, we will use the `@react-navigation` library.

import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { NavigationContainer } from "@react-navigation/native";

const Stack = createNativeStackNavigator();

export const AppNavigator = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="BrowseScreen" component={BrowseScreen} />
        <Stack.Screen name="PlayerScreen" component={PlayerScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

We also need to create our "database" of movies. I did this statically, but many of you may use an API to get this data.

const DUMMY_VIDEOS = Array.from({ length: 10 }).map((_, index) => ({
  id: index.toString(),
  title: `Video ${index + 1}`,
  source: require("./pathToFile.mp4"),
  thumbnail: require("./pathToFile.jpg"),
}));

We will make our application play the video in a preview at the top of the screen when hovering over a thumbnail. There will also be a "play" button that will start the movie and, upon clicking, will take the user to a separate screen with that movie. However, let's start by adding a list of movies.

export const BrowseScreen = () => {
  // ...

  const renderItem = ({ item }) => (
    <TouchableOpacity>
      <Image source={item.thumbnail} resizeMode="cover" />
      <Text>{item.title}</Text>
    </TouchableOpacity>
  );

  return (
    <ScrollView>
      <Text>Last Added</Text>
      <FlatList
        ref={flatListRef}
        data={DUMMY_VIDEOS}
        renderItem={renderItem}
        keyExtractor={(item) => item.id}
        horizontal
      />
    </ScrollView>
  );
};

Next, let's add the video preview at the top of the screen.

// ...

// Default select the first one movie
const [selectedVideo, setSelectedVideo] = useState(DUMMY_VIDEOS[0]);

const renderItem = ({ item }) => (
  <TouchableOpacity onFocus={() => setSelectedVideo(item)}>
    {/* ... */}
  </TouchableOpacity>
);

return (
  <ScrollView>
    <Video
      source={selectedVideo.source}
      resizeMode="cover"
      repeat
      playInBackground={false}
    />
    <Text>Last Added</Text>
    <FlatList
      {/* ... */}
    />
  </ScrollView>
);

And let's add the "play" button. Remember how focusing works on Apple TV, we need to wrap the "play" button component and our list of movies with the TVFocusGuideView component. To avoid errors on Fire TV and AndroidTV we should also set nextFocusUp at FlatList to make sure, that "play" button is focused after click "up" on the remote.

// ...

const playButtonRef = useRef<View>(null);

const [playButtonNativeID, setPlayButtonNativeID] = useState<number | null>(
  null,
);

useEffect(() => {
  if (playButtonRef.current) {
    // findNodeHandle imported from 'react native', it gives us a native id of this button
    setPlayButtonNativeID(findNodeHandle(playButtonRef.current));
  }
}, []);

// ...

const renderItem = ({item}) => (
  <TouchableOpacity
    nextFocusUp={playButtonNativeID ?? undefined}
    >
    {/*...*/}
  </TouchableOpacity>
);

// ...

 <View>
  <Video
    {/* ... */}
  />
  <View>
    {/* To make sure TVFocusGuideView works correctly, we can add a width property at styles */}
    <TVFocusGuideView autoFocus style={{width: DEVICE_WIDTH}}>
      <Pressable
        ref={playButtonRef}
        // Add the hasTVPreferredFocus property to ensure this element is focused by default. Remember to use this only once per screen
        hasTVPreferredFocus={true}
      >
        <Text>Play</Text>
      </Pressable>
    </TVFocusGuideView>
  </View>
 </View>
 <Text>Last Added</Text>
 <TVFocusGuideView autoFocus>
  <FlatList
    {/* ... */}
  />
 </TVFocusGuideView>

// ...

Let's now add the ability to navigate between screens.

import { useNavigation } from "@react-navigation/native";

// ...

const navigation = useNavigation();

const handleNavigate = () => {
  navigation.navigate("PlayerScreen", { videoSource: selectedVideo.source });
};

const renderItem = ({ item }) => (
  <TouchableOpacity
    onPress={() => {
      handleNavigate();
    }}
    // ...
  >
    {/* ... */}
  </TouchableOpacity>
);

return (
  // ...
  <Pressable
    onPress={() => {
      handleNavigate();
    }}
    // ...
  >
    <Text>Play</Text>
  </Pressable>
  // ...
);

Now the application with added styles, should look like this

Do you feel like you have a long way to go and need to add tons of features to finish your app?

Feel free to contact us. We thrive on such challenges and can provide you with a free ballpark quote for building your app.

Finally, we can add the PlayerScreen component. We want to add custom controls, which are visible everytime when user clicked some buttons on remote

import { useTVEventHandler, TVFocusGuideView, HWEvent } from "react-native";
import Video from "react-native-video";
// ...

export const PlayerScreen = ({ route }) => {
  const { videoSource } = route.params;

  const hideControlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);

  const [controlsVisible, setControlsVisible] = useState(true);

  // open controls for 3 sec
  const controlsOpenTimer = useCallback(() => {
    setControlsVisible(true);

    if (hideControlsTimeoutRef.current) {
      clearTimeout(hideControlsTimeoutRef.current);
    }

    hideControlsTimeoutRef.current = setTimeout(() => {
      setControlsVisible(false);
    }, 3000);
  }, []);

  const myTVEventHandler = (evt: HWEvent) => {
    if (
      ["playPause", "select", "up", "down", "left", "right"].includes(
        evt.eventType,
      )
    ) {
      controlsOpenTimer();
    }
  };

  // handler for remote events
  useTVEventHandler(myTVEventHandler);

  return (
    <Video
      source={videoSource}
      resizeMode="cover"
      repeat
      playInBackground={false}
    />
  );
};

Now we have saved in the state whether the controls should be visible, so we can easily add 'play/pause', 'go back', 'progress bar' and 'movie time' controls

// ...

const formatTime = (seconds: number) => {
  const minutes = Math.floor(seconds / 60);
  const secs = Math.floor(seconds % 60);
  return `${minutes}:${secs.toString().padStart(2, '0')}`;
};

// ...

const videoRef = useRef<VideoRef>(null);

const [paused, setPaused] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
// Preview time is important, when we long press right or left. We will update preview time, and after we unpress, we will update `currentTime`
const [previewTime, setPreviewTime] = useState(0);
const [duration, setDuration] = useState(0);

const controlsOpenTimer = useCallback(() => {
  // ...
}, []);

const seek = (time: number) => {
  const newTime = Math.max(0, Math.min(duration, time));
  videoRef.current?.seek(newTime);
  setCurrentTime(newTime);
  setPreviewTime(newTime);
  controlsOpenTimer();
};

// for short btn press seek time in seconds
const seekTime = 5;
// for long btn press seek time in seconds
const longSeekTime = 15;

const seekForward = (type: 'press' | 'longPress' = 'press') => {
  seek(currentTime + (type === 'press' ? seekTime : longSeekTime));
  // reset timer
  controlsOpenTimer();
};

const seekBackward = (type: 'press' | 'longPress' = 'press') => {
  seek(currentTime - (type === 'press' ? seekTime : longSeekTime));
  controlsOpenTimer();
};

const togglePausePlay = () => {
  setPaused(prev => !prev);
  controlsOpenTimer();
};

return (
  <View>
    <Video
      ref={videoRef}
      source={videoSource}
      paused={paused}
      onProgress={({ currentTime }) => {
        setCurrentTime(currentTime);
        setPreviewTime(currentTime);
      }}
      onLoad={({ duration }) => setDuration(duration)}
      repeat
    />
    {controlsVisible && (
      <View>
        <TVFocusGuideView autoFocus>
          <Pressable
            onPress={() => navigation.goBack()}>
            <GoPreviousSvg />
          </Pressable>
        </TVFocusGuideView>

        <TVFocusGuideView autoFocus>
          <Pressable>
            {paused ? (
              <PlaySvg />
            ) : (
              <PauseSvg />
            )}
          </Pressable>
        </TVFocusGuideView>

        <View>
          <TVFocusGuideView autoFocus>
            <View style={{ 
              width: sliderWidth,
              marginHorizontal: (DEVICE_WIDTH - sliderWidth) / 2
            }} />
            <Pressable
              style={({ focused }) => [
                {
                  left:
                    (previewTime / duration) * (sliderWidth - thumbWidth) ||
                    0,
                },
              ]}
            />
          </TVFocusGuideView>
        </View>

        <View>
          <Text>
            {formatTime(previewTime)} / {formatTime(duration)}
          </Text>
        </View>
      </View>
    )}
  </View>
)

To complete the implementation, we should also add event listeners for remote control buttons to show and hide the controls and automatically detect right/left button presses to seek the video.

const handleLongPress = (direction: 'left' | 'right') => {
  const seekAmount = direction === 'left' ? -longSeekTime : longSeekTime;

  if (Platform.OS === 'ios') {
    // IOS: long press is triggered on start and finish pressing
    if (!keyPressed) {
      setKeyPressed(true);
      // Need to remember to pause the video because then we update preview time there and on video progress
      setPaused(true);
      longPressTimeoutRef.current = setInterval(() => {
        setPreviewTime(prevPreviewTime => prevPreviewTime + seekAmount);
        controlsOpenTimer();
      }, 200);
    } else {
      setKeyPressed(false);
      setPaused(false);
      clearInterval(longPressTimeoutRef.current || undefined);
      seek(previewTime);
    }
  } else {
    // ANDROID: long press is triggered from time to time while pressing
    const newPreviewTime = previewTime + seekAmount;
    setKeyPressed(true);
    setPaused(true);
    setPreviewTime(newPreviewTime);
    clearInterval(longPressTimeoutRef.current ?? undefined);
    longPressTimeoutRef.current = setTimeout(() => {
      setKeyPressed(false);
      setPaused(false);
      seek(newPreviewTime);
    }, 600);
  }
};

const myTVEventHandler = (evt: HWEvent) => {
  const {eventType} = evt;

  if (eventType === 'playPause') {
    setPaused(prev => !prev);
  }

  if (
    [
      'playPause',
      'select',
      'up',
      'down',
      'left',
      'right',
      'longLeft',
      'longRight',
    ].includes(eventType)
  ) {
    controlsOpenTimer();
  }

  switch (eventType) {
    case 'select':
      togglePausePlay();
      break;
    case 'left':
      seekBackward();
      break;
    case 'right':
      seekForward();
      break;
    case 'longLeft':
      handleLongPress('left');
      break;
    case 'longRight':
      handleLongPress('right');
      break;
    default:
      break;
  }
};

useTVEventHandler(myTVEventHandler);

Final player should look like this

Summary

In this article, we showed how to create a streaming app for TV using react-native-tvos and react-native-video. We configured the environment for Fire TV, Android TV, and Apple TV. You can find the full code on GitHub. If you have any questions, feel free to join our Discord!

React Native
React Native Video
tvOS
Fire TV
Kamil Moskała
No items found.
Do you need help with developing react solutions?

Leave your contact info and we’ll be in touch with you shortly

Leave contact info
Become one of our 10+ ambassadors and earn real $$$.
By clicking “Accept all”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.