Skip to content

Embedded Clips#

Configuring a Storyteller Embedded Clips Component#

The Storyteller SDK provides a component to embed a clips experience in your layout.

Props#

interface Configuration {
  collection?: string;
  category?: string;
  clipId?: string;
  openedReason?: StorytellerOpenReason;
  theme?: Theme;
  topInsets?: number; // Android only
  context?: StorytellerAnalyticsContext;
  adConfiguration?: StorytellerClipsAdConfiguration;
}

interface TopLevelBackTappedEvent {
  canGoBack: boolean;
}

interface DataLoadCompletedEvent {
  success: boolean;
  error: string;
  dataCount?: number;
}

interface CanGoBackChangedEvent {
  canGoBack: boolean;
}

// Component Props
configuration?: Configuration;
topLevelBackButtonEnabled?: boolean;
onTopLevelBackTapped?: (event: TopLevelBackTappedEvent) => void;
onDataLoadStarted?: () => void;
onDataLoadCompleted?: (event: DataLoadCompletedEvent) => void;
onCanGoBackChanged?: (event: CanGoBackChangedEvent) => void;
style?: ViewStyle;

To customize the layout of a component you can use the following props:

  • configuration.collection: the collection ID to be displayed

  • configuration.category: specify which category to show when opening the collection. If category is not specified or the wrong value is set, we default to the first clip

  • configuration.clipId: specify which clipId to show when opening the collection. If clipId is not specified or the wrong value is set, we default to the first clip

  • configuration.openedReason - specify the reason why collection was reloaded which is used for analytics. If nil, then openedReason will be handled internally.

  • configuration.theme - optional per-view Storyteller theme.

  • configuration.context - optional string key/value analytics context attached to events from this view.

  • configuration.topInsets - specify the top insets of the Clips view. This is useful when the Clips view is embedded in a view that has a top inset, such as a tab bar. This property is only available on Android.

  • configuration.adConfiguration - optional per-presentation Clips ad placement controls. Set preRollEnabled and/or bottomBannerEnabled to true to opt this embedded Clips view into those placements when Clips ads are available for the tenant. On Android, betweenClipsAdProviderOrder accepts StorytellerAdProvider.vast, .gam, and .admob: omit it or pass null for native order, use [] to disable standard between-Clips ads, or pass an array to set the allowlist and fallback order. IMA opening pre-roll is separate from this list. iOS ignores the provider-order property. Omitted flags are treated as false.

  • topLevelBackButtonEnabled - whether the back button should be displayed when the player is at the top level of the collection. Defaults to true

  • onDataLoadStarted?: () => void - use it to show any loading indicators, like a spinning wheel on a tab bar item.

  • onDataLoadCompleted?: (event: DataLoadCompletedEvent) => void - this callback tells you that loading the data has finished. The event contains success (boolean), error (string), and an optional dataCount (number). The current Embedded Clips native bridges omit dataCount; list and Card components include it.

  • onTopLevelBackTapped?: (event: TopLevelBackTappedEvent) => void - in case you set topLevelBackButtonEnabled to true, this callback will be called when the user taps on the back button when at the top level of the StorytellerEmbeddedClipsView. The event.canGoBack property tells whether the back button action is managed by the SDK. When false, the integrating app can handle the back button action. When true, the SDK will manage the action by popping the top level category.

  • onCanGoBackChanged?: (event: CanGoBackChangedEvent) => void - this callback is used to handle the result from a StorytellerEmbeddedClipsView.getCanGoBack() call. On Android and iOS 11.6.0+, event.canGoBack is true only when the SDK can pop an internally pushed category screen; it is false at the collection root or before nested navigation is ready.

Adding it in the layout#

Starting silently#

Initialize the SDK, then await StorytellerSdk.mute() before mounting the view. Calling mute in a focus callback after mounting can be too late to prevent initial audio. For example, after initialization has completed:

const [showClips, setShowClips] = useState(false);

const showMutedClips = async () => {
  try {
    await StorytellerSdk.mute();
    setShowClips(true);
  } catch (error) {
    // Display an error; leave the view unmounted.
    console.error('Unable to start muted Clips', error);
  }
};

return (
  <View style={{ flex: 1 }}>
    <Button title="Show muted Clips" onPress={showMutedClips} />
    {showClips && (
      <StorytellerEmbeddedClipsView
        style={{ flex: 1 }}
        configuration={{ collection: collectionId }}
      />
    )}
  </View>
);

Keep the normal willShow() and willHide() lifecycle handling described below. While the view is mounted, await StorytellerSdk.mute() changes audio without pausing video; await StorytellerSdk.unmute() follows the normal device and persistence rules. See audio commands for completion, initialization errors and later user changes.

A host can await mute and then display a prompt over the mounted view while video continues. Keep the same embedded view mounted when showing or dismissing the prompt. Dismissing a prompt does not restore audio automatically; call unmute explicitly when appropriate.

Layout example#

To put it in your custom view hierarchy you will need to add the following code:

import { useRef } from 'react';
import { View, StyleSheet } from 'react-native';
import {
  StorytellerAdProvider,
  StorytellerEmbeddedClipsView,
  type StorytellerEmbeddedClipsViewInterface,
} from '@getstoryteller/react-native-storyteller-sdk';

function MyComponent() {
  const embeddedClipsRef = useRef<StorytellerEmbeddedClipsViewInterface>(null);

  return (
    <View style={styles.container}>
      <StorytellerEmbeddedClipsView
        ref={embeddedClipsRef}
        style={styles.embeddedComponent}
        configuration={{
          collection: 'collection-id',
          category: 'category-id',
          clipId: 'clip-id',
          openedReason: 'opened-reason',
          adConfiguration: {
            preRollEnabled: true,
            bottomBannerEnabled: true,
            betweenClipsAdProviderOrder: [
              StorytellerAdProvider.vast,
              StorytellerAdProvider.gam,
            ],
          },
        }}
        topLevelBackButtonEnabled={false}
        onDataLoadStarted={() => {
          console.log('Data load started');
        }}
        onDataLoadCompleted={(event) => {
          console.log('Data load completed:', event.success, event.error, event.dataCount);
        }}
        onTopLevelBackTapped={(event) => {
          console.log('Top level back tapped, canGoBack:', event.canGoBack);
        }}
        onCanGoBackChanged={(event) => {
          console.log('Can go back changed:', event.canGoBack);
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  embeddedComponent: {
    flex: 1,
  },
});
// somewhere inside a react component
const ref = useRef<StorytellerEmbeddedClipsView | null>(null);

useEffect(() => {
  if (ref.current) {
    ref.current.create();
  }
}, []);

return (
  <SafeAreaView style={styles.container}>
    <StorytellerEmbeddedClipsView
      configuration= {{
        collection: 'collection-id',
        category: 'category-id',
        clipId: 'clip-id',
        openedReason: 'opened-reason',
      }}
      topLevelBackButtonEnabled={true}
      onTopLevelBackTapped={(canGoBack) => {
        console.log(`topLevelBackTapped canGoBack: ${canGoBack}`);
      }}
      onDataLoadStarted={() => {
        console.log('onDataLoadStarted');
      }}
      onDataLoadCompleted={(success, error) => {
        console.log('onDataLoadCompleted', success, error);
      }}
      onCanGoBackChanged={(canGoBack) => {
        console.log('onCanGoBackChanged', canGoBack);
      }}
      style={styles.clips}
      ref={ref}
    />
  </SafeAreaView>
);

Auto-pause#

If you want to pause the clips when navigating between tabs or when the app goes to background, you can use the willShow() and willHide() methods:

import { useRef, useCallback } from 'react';
import { View, StyleSheet } from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import {
  StorytellerEmbeddedClipsView,
  type StorytellerEmbeddedClipsViewInterface,
} from '@getstoryteller/react-native-storyteller-sdk';

export default function EmbeddedClipsTab() {
  const embeddedClipsRef = useRef<StorytellerEmbeddedClipsViewInterface>(null);

  // Handle tab focus/blur to control video playback
  useFocusEffect(
    useCallback(() => {
      // Tab is focused - show the embedded clips
      embeddedClipsRef.current?.willShow();

      return () => {
        // Tab is blurred - hide the embedded clips
        embeddedClipsRef.current?.willHide();
      };
    }, [])
  );

  return (
    <View style={styles.container}>
      <StorytellerEmbeddedClipsView
        ref={embeddedClipsRef}
        style={styles.embeddedComponent}
        configuration={{
          collection: 'your-collection-id',
        }}
        topLevelBackButtonEnabled={false}
        onDataLoadCompleted={(event) =>
          console.log('Embedded clips data load completed:', event)
        }
        onTopLevelBackTapped={(event) =>
          console.log('Embedded clips top level back tapped:', event)
        }
        onCanGoBackChanged={(event) =>
          console.log('Embedded clips can go back changed:', event)
        }
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  embeddedComponent: {
    flex: 1,
  },
});
reference: StorytellerEmbeddedClipsView | null = null;

constructor(props: EmbeddedClipsScreenProps) {
  super(props);
  this.state = {
    appState: AppState.currentState as AppStateStatus,
    collectionId: 'example',
  };
}

componentDidMount(): void {
  this.props.navigation.addListener('focus', this._onFocus);
  this.props.navigation.addListener('blur', this._onBlur);
  AppState.addEventListener('change', this._handleAppStateChange);
}

componentWillUnmount(): void {
  this.props.navigation.removeListener('blur', this._onBlur);
  this.props.navigation.removeListener('focus', this._onFocus);
}

_handleAppStateChange = (nextAppState: AppStateStatus) => {
  if (
    this.state.appState.match(/inactive|background/) &&
    nextAppState === 'active'
  ) {
    this.reference?.willShow();
  }
  this.setState({ appState: nextAppState });
};

_onFocus = () => {
  this.reference?.willShow();
};

_onBlur = () => {
  this.reference?.willHide();
};

render() {
  return (
    <SafeAreaView style={styles.container}>
      <StorytellerEmbeddedClipsView
        collection={this.state.collectionId}
        style={styles.clips}
        ref={(ref) => {
          if (ref) {
            this.reference = ref;
          }
        }}
      />
    </SafeAreaView>
  );
}

Reload Data#

If you want to reload the data programmatically, you can use the reloadData() method:

import { useRef } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import {
  StorytellerEmbeddedClipsView,
  type StorytellerEmbeddedClipsViewInterface,
} from '@getstoryteller/react-native-storyteller-sdk';

function MyComponent() {
  const embeddedClipsRef = useRef<StorytellerEmbeddedClipsViewInterface>(null);

  const handleReload = () => {
    embeddedClipsRef.current?.reloadData();
  };

  return (
    <View style={styles.container}>
      <Button title="Reload Data" onPress={handleReload} />
      <StorytellerEmbeddedClipsView
        ref={embeddedClipsRef}
        style={styles.embeddedComponent}
        configuration={{
          collection: 'collection-id',
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  embeddedComponent: {
    flex: 1,
  },
});
const ref = useRef<StorytellerEmbeddedClipsView | null>(null);

const reloadData = () => {
  if (ref.current) {
    ref.current.reloadData();
  }
}

return (
  <SafeAreaView style={styles.container}>
    <StorytellerEmbeddedClipsView
      collection={'collection-id'}
      style={styles.clips}
      ref={ref}
    />
  </SafeAreaView>
);

Available Methods#

The StorytellerEmbeddedClipsViewInterface provides the following methods:

  • getCanGoBack() - Triggers the onCanGoBackChanged callback with the current navigation state on Android and iOS 11.6.0+
  • goBack() - Navigates back in the Clips navigation stack on Android. The iOS native SDK does not currently expose a public imperative back command, so this command is intentionally a no-op on iOS.
  • willShow() - Resume video playback (call when view becomes visible)
  • willHide() - Pause video playback (call when view becomes hidden)
  • reloadData() - Reload the clips collection data