React Native Integration Guide

Integration guide for the StreamLayer SDK in bare React Native projects using the old architecture (RN 0.72+). Covers installation, native iOS and Android configuration, THEOplayer setup, SDK initialization, and the StreamLayerView component.

React Native Integration Guide (Old Architecture)

Note: This documentation is for the old React Native architecture. For the new architecture, see React Native New Architecture Integration Guide.

This documentation is for the old architecture of React Native.

Background

The React Native SDK should be integrated if the client host app is built using React. Please ensure you are familiar with the Architecture Overview before proceeding with this integration.

Prerequisites

System Requirements

iOS Requirements
  • Xcode 15.3
  • Swift 5.9
  • iOS 15+
Android Requirements
  • Android Studio 4.0+
  • Kotlin 1.7+ or Java 8+
  • Minimum SDK version 21

Obtain an SDK API Key

You will need an SDK API Key to integrate StreamLayer into your application. If you do not already have one, please follow the instructions below.

Provide us with your email address to create a new organization for you on our platform, including setting up a personal dashboard for your account. You will then receive an invitation email with a link to the StreamLayer Studio and your authentication credentials. After gaining access to the StreamLayer Studio, you can generate an API key in the development section of the StreamLayer Studio. This API key authenticates your application with our servers and is required for all interactions.

Integration Example

You can find a complete StreamLayer SDK integration example in our repo.
Or you can follow our video guide:

Installation

Install the necessary packages with the following command:

npm

npm install react-native-streamlayer react-native-safe-area-context

yarn

yarn add react-native-streamlayer react-native-safe-area-context

Native iOS Configuration

Update Cocoapods Dependencies

Navigate to the ios directory and install the dependencies:

cd ios && pod install && cd ..

Upgrade minimum Ios Target

Update the minimum iOS version in the Podfile.

platform :ios, '15.0'

Install TheoPlayer

Install packages

npm


yarn


Navigate to the ios/ directory and install the dependencies:

cd ios/ && pod install && cd ../

Add dependencies

Add dependencies inandroid/app/build.gradle

dependencies {
		...your dependencies
    implementation 'com.google.android.gms:play-services-cast-framework:21.4.0'
    implementation "com.google.android.gms:play-services-ads-identifier:18.0.1"	
}

Add metadata

Add metadata to AndroidManifest.xml

    <meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:value="com.googlecastoptionprovider.CastOptionsProvider" />

Android Configuration

Create CastOptionsProvider.kt

Create a file named CastOptionsProvider.kt in android/app/src/main/java/com/sl/expo/app/ with the following content:

package com.googlecastoptionprovider

import android.content.Context
import com.google.android.gms.cast.framework.CastOptions
import com.google.android.gms.cast.framework.OptionsProvider
import com.google.android.gms.cast.framework.SessionProvider
import com.google.android.gms.cast.framework.CastOptions.Builder

class CastOptionsProvider : OptionsProvider {
   override fun getCastOptions(context: Context): CastOptions {
      return Builder()
            .setReceiverApplicationId("CC1AD845") 
            .build()
   }

   override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
      return null
   }
}

StreamLayer SDK Setup for React Native

Add API Keys to .env

  1. Install react-native-config:
npm install react-native-config
yarn add react-native-config
  1. In the ios directory, run:
pod install
  1. Add the following line to android/app/build.gradle:
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
  1. Create a .env file in your project’s root directory and add your API key:
SL_SDK_API_KEY="your_api_key_here"

Implement <SafeAreaProvider> at the top of your app.

  1. For example I gonna implement this at index.js/index.tsx
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
import { SafeAreaProvider } from 'react-native-safe-area-context';

const AppWithProvider = () => (
  <SafeAreaProvider>
    <App />
  </SafeAreaProvider>
);

AppRegistry.registerComponent(appName, () => AppWithProvider);

Add imports

import React, { useState, useEffect, useRef, ReactElement } from 'react';
import {
    StyleSheet,
    View,
    ScrollView,
    Dimensions,
    Text,
    Image,
    Pressable,
} from 'react-native';
import { 
    StreamLayer, 
    StreamLayerView, 
    StreamLayerViewPlayer, 
    StreamLayerViewOverlayLandscapeMode, 
    StreamLayerViewConfiguration, 
    StreamLayerViewNotificationFeature, 
    StreamLayerDemoEvent, 
    StreamLayerTheme 
} from 'react-native-streamlayer';
import { 
    PlayerConfiguration, 
    SourceDescription, 
    PlayerEventType, 
    THEOplayer, 
    THEOplayerView 
} from 'react-native-theoplayer';
import { UiContainer, 
    CenteredControlBar, 
    SkipButton, 
    PlayButton, 
    DEFAULT_THEOPLAYER_THEME 
} from '@theoplayer/react-native-ui';
import {
    SafeAreaView,
    useSafeAreaInsets,
  } from 'react-native-safe-area-context';

Add variables

  class LBarState {
    slideX: number;
    slideY: number;

    constructor(slideX: number, slideY: number) {
        this.slideX = slideX
        this.slideY = slideY
    }
}

const playerConfig: PlayerConfiguration = {
    license: undefined,
};

const source: SourceDescription = {
    sources: [
        {
            src: "https://cdn.theoplayer.com/video/elephants-dream/playlist-single-audio.m3u8",
            type: "application/x-mpegurl"
        },
    ],
};

const insets = useSafeAreaInsets()

const playerHeight = isScreenPortrait() ? 300 : Dimensions.get('screen').height;

const streamLayerViewPlayer: StreamLayerViewPlayer = {
    get volume() {
        return 0.0
    },
    set volume(value) {

    },
}

const viewConfig = getViewConfig()

const [volumeBeforeDucking, setVolumeBeforeDucking] = useState<number | undefined>(undefined)
const [isPortrait, setPortrait] = useState<boolean>();
const [player, setPlayer] = useState<THEOplayer | undefined>(undefined);
const [lbarState, setLbarState] = useState(new LBarState(0, 0));
const [events, setEvents] = useState<Array<StreamLayerDemoEvent>>()
const [currentEventId, setCurrentEventId] = useState<String>()
const [isInitialized, setInitialized] = useState(false);
const viewRef = useRef<StreamLayerView>(null);

var scrollItems = new Array<ReactElement>();
if (events !== undefined) {
  events.forEach((event) => {
      scrollItems.push(
          <Pressable key={event.id} onPress={() => createEventSession(event.id)}>
              <View style={styles.eventRow}>
                  {event.previewUrl !== undefined && (
                      <Image source={{ uri: event.previewUrl }}
                          style={styles.eventRowImage} />
                  )}
                  <Text style={styles.eventRowTitle} numberOfLines={1} ellipsizeMode='tail'>{event.title}</Text>
              </View>
          </Pressable>
      )
  })
}

var currentEvent: StreamLayerDemoEvent | undefined;
if (events !== undefined && currentEventId !== undefined) {
  currentEvent = events.find((event) => {
      return event.id == currentEventId
  })
}

Add Functions

useEffect(() => {
        
  setPortrait(isScreenPortrait())
  const initialize = async () => {

      try {
          checkInitialized();
      } catch (error) {
          console.error("Error initializing:", error);
      }
  };

  initialize();

  const subscription = Dimensions.addEventListener('change', ({ window, screen }) => {
      setPortrait(window.height > window.width)
  });


  return () => subscription?.remove()


}, []);


function getViewConfig(): StreamLayerViewConfiguration {
  return {
      viewNotificationFeatures: new Array(
          StreamLayerViewNotificationFeature.Games,
          StreamLayerViewNotificationFeature.Chat,
          StreamLayerViewNotificationFeature.WatchParty,
          StreamLayerViewNotificationFeature.Twitter
      ),
      isGamesPointsEnabled: true,
      isGamesPointsStartSide: false,
      isLaunchButtonEnabled: true,
      isMenuAlwaysOpened: false,
      isMenuLabelsVisible: true,
      isMenuProfileEnabled: true,
      isTooltipsEnabled: true,
      isWatchPartyReturnButtonEnabled: true,
      isWhoIsWatchingViewEnabled: true,
      isOverlayExpandable: true,
      overlayHeightSpace: 300,
      overlayWidth: 0,
      overlayLandscapeMode: StreamLayerViewOverlayLandscapeMode.Start
  }
}

function isScreenPortrait(): boolean {
  return Dimensions.get('window').height > Dimensions.get('window').width
}


const onReady = (player: THEOplayer) => {
  setPlayer(player);
  player.autoplay = true
  player.source = source;
  player.addEventListener(PlayerEventType.ERROR, console.log);
}


const checkInitialized = async () => {
  try {
          await StreamLayer.initSdk({
              isLoggingEnabled: true,
              theme: StreamLayerTheme.Green,
              sdkKey: "YOUR SDK KEY",
            },false)
          checkAuth()
          loadDemoEvents()
          const inited = await StreamLayer.isInitialized()
          setInitialized(inited)

  } catch (e) {
      console.error(e);
  }
}

const loadDemoEvents = async () => {
  try {
      const events = await StreamLayer.getDemoEvents("2022-01-01")
      setEvents(events)
      if (events !== undefined && events !== null && events.length > 0) {
          createEventSession(events[0].id)
      }
  } catch (e) {
      console.error("PlayerScreen loadDemoEvents error", e);
  }
}

const checkAuth = async () => {
  try {
      const isUserAuthorized = await StreamLayer.isUserAuthorized()
      if (!isUserAuthorized) {
          await StreamLayer.useAnonymousAuth();
      }
  } catch (e) {
      console.error(e);
  }
}

const createEventSession = async (id: string) => {
  try {
      await StreamLayer.createEventSession(id);
      console.log(`Created a new event with id ${id}`);
      setCurrentEventId(id)
  } catch (e) {
      console.error(e);
  }
};

Add functions for view event listener

    const onRequestStream = (id: string) => {
        console.log("onRequestStream id=" + id)
        createEventSession(id)
    }

    const onLBarStateChanged = (slideX: number, slideY: number) => {
        console.log("onLBarStateChanged slideX=" + slideX + " slideY=" + slideY)
        setLbarState(new LBarState(slideX, slideY));
    }

    const onRequestAudioDucking = (level: number) => {
        console.log("onRequestAudioDucking level=" + level)
    }

    const onDisableAudioDucking = () => {
        console.log("onDisableAudioDucking")
    }

Add Views for StreamLayer and StreamLayer Events

    return (
        <SafeAreaView style={{...styles.container, marginTop: insets.top }}  edges={['top']}>
            {(isPortrait) &&
                <View style={{ flex: 1, marginTop: playerHeight  - insets.top }}>
                    {currentEvent !== undefined && (
                        <Text style={styles.eventTitle}>{currentEvent.title}</Text>
                    )}
                    <ScrollView style={{ flex: 1 }}>
                        {scrollItems}
                    </ScrollView>
                </View>
            }
            
            {isInitialized && 
                <StreamLayerView
                    style={StyleSheet.absoluteFillObject}
                    ref={viewRef}
                    config={viewConfig}
                    applyWindowInsets={false}
                    onRequestStream={onRequestStream}
                    onLBarStateChanged={onLBarStateChanged}
                    onRequestAudioDucking={onRequestAudioDucking}
                    onDisableAudioDucking={onDisableAudioDucking}
                    player={streamLayerViewPlayer}
                    playerView={
                        <THEOplayerView config={playerConfig} onPlayerReady={onReady}
                            style={{
                                width: Dimensions.get('screen').width - lbarState.slideX,
                                height: playerHeight-lbarState.slideY,
                                paddingTop: 0,
                            }}>
                            {player !== undefined && (
                                <UiContainer
                                    theme={DEFAULT_THEOPLAYER_THEME}
                                    player={player}
                                    center={
                                        <CenteredControlBar
                                            left={<SkipButton skip={-10} />}
                                            middle={<PlayButton />}
                                            right={<SkipButton skip={10} />}
                                        />
                                    }
                                />
                            )}
                        </THEOplayerView>
                    }
                />
            }

        </SafeAreaView>
    )


const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: 'gray',
    },
    eventTitle: {
        color: 'white',
        fontSize: 24,
        margin: 4
    },
    eventRowTitle: {
        flex: 1,
        color: 'white',
        fontSize: 16,
        margin: 4
    },
    eventRow: {
        flexDirection: 'row',
        margin: 4,
        padding: 4,
        height: 58,
        justifyContent: 'flex-start',
        alignItems: 'center',
        borderWidth: 1,
        borderColor: 'black',
    },
    eventRowImage: {
        width: 100,
        height: 50
    },
    overlay: {
        paddingTop: 500,
        backgroundColor: '#00000000',
        flex: 1,
    }
});