@dicetechnology/react-native-vesper-sdk - v2.0.6

react-native-vesper-sdk

React native wrapper for iOS/Android native VesperSDK

Installation

npm install @dicetechnology/react-native-vesper-sdk

The @dicetechnology/react-native-vesper-sdk is a wrapper around native iOS/Android VesperSDK which are hosted in private repositories. It requires basic authentication to download and install.

iOS VesperSDK auth

You will need to provide your credentials in ~/.netrc file to be able to authenticate during pod install phase. Add this inside your ~/.netrc file or create new file if you do not have one:

machine d1st2jzonb6gjl.cloudfront.net
login <login>
password <password>

Android VesperSDK auth

You will need to provide your Cloudfront authorisation token in the project level build.gradle file and have the Google, Central, Mux, and Endeavor Streaming Maven repositories included.

allprojects {
repositories {
google()
mavenCentral()
maven {
// Mux repository
url "https://muxinc.jfrog.io/artifactory/default-maven-release-local"
content {
includeGroupByRegex "com\\.mux.*"
}
}
maven {
// Endeavor Streaming repository
url "https://d1yvb7bfbv0w4t.cloudfront.net/"
credentials { username authToken }
}
}
}

Once you have your authorisation token, add it to $HOME/.gradle/gradle.properties

authToken=your_auth_token

Note: Adding your authorisation token directly to the build.gradle file in the root of your project will also work, but this is not recommended. The build.gradle file in the root of your project is typically committed to your source control (Git) repository and this will expose your authorisation token.

Android Gradle configuration

Add the following property to your gradle.properties file to disable the dexing artifact transform:

For Gradle versions earlier than 8.4:
android.enableDexingArtifactTransform=false
For Gradle versions 8.4 and later:
android.useFullClasspathForDexingTransform=true

Usage

Initialization

import {
RNPlayerView,
VesperSdk,
RNEnvironment,
} from '@dicetechnology/react-native-vesper-sdk';
import type { AuthManager } from '@dicetechnology/react-native-vesper-sdk';

class VesperAuthManager implements AuthManager {
public async getAuthToken(): Promise<string> {
// return authToken from internal storage; after any token refresh,
// store & return the new valid one here
return 'auth_token';
}

public async getRefreshToken(): Promise<string> {
return 'refresh_token';
}

public async refreshAuthToken(authToken: string): Promise<string> {
// refresh and store authToken, then return the refreshed one
return 'refreshed_auth_token';
}
}

VesperSdk.setup({
apiConfig: {
realm: 'dce.REALM_HERE',
environment: RNEnvironment.PRODUCTION,
apiKey: 'API_KEY_HERE',
contentLanguageCode: 'en', // optional
},
authManager: new VesperAuthManager(),
});

Authentication

You may have noticed that the SDK takes an AuthManager as part of its configuration.

Ownership of the authentication flow is left to the application using the Vesper SDK. Once the app has authenticated with the user's credential, the SDK via the AuthManager will retrieve the user's authToken and refreshToken to facilitate interactions with the Vesper platform.

Token Refresh

When the authToken expires, the SDK will use the refreshAuthToken method on the AuthManager to request a new token from the application. It is the application's responsibility to request and provide the updated token back to the Vesper SDK.

In the event of an authToken refresh error, the current playback session will be stopped. The expectation will be on the app to prompt the user to sign in again.

The SDK offers a mechanism to listen for this event. We'll take a closer look at this in the Listen section.

Creating the Player

Once the Vesper SDK has been configured, you may move on to the RNPlayerView and create it for managing playback.

The RNPlayerView will be our entry point for everything playback-related--as we will see throughout this guide. The simplest way to create it is as follows:

export default function App() {
const ref = React.createRef<RNPlayerView>();

return (
<View style={styles.container}>
<RNPlayerView
ref={ref}
style={styles.box}
/>
</View>
);
}

Load a video

import type { RNLoadData } from '@dicetechnology/react-native-vesper-sdk';
import { RNBeaconViewType } from '@dicetechnology/react-native-vesper-sdk';

export default function App() {
const ref = React.createRef<RNPlayerView>();

const handleLoadPress = () => {
ref.current?.load({
source: {
id: '<video_id>',
isLive: false,
startAt: 100,
preferredAudioTracks: ['en'],
preferredSubtitles: ['en'],
beaconContext: { viewType: RNBeaconViewType.STANDARD },
preferDownloaded: false,
allowClientSideAds: true,
},
autoPlay: true,
});
};

return (
<View style={styles.container}>
<RNPlayerView
ref={ref}
style={styles.box}
/>
<View style={{ flexDirection: 'row' }}>
<Button
title="Load"
onPress={handleLoadPress}
/>
</View>
</View>
);
}

Load a collection (playlist | season | watchlist)

import { RNWatchContextType } from '@dicetechnology/react-native-vesper-sdk';

const handleLoadPress = () => {
ref.current?.load({
source: {
id: '<video_id>',
isLive: false,
startAt: 0,
watchContext: {
id: '<collection_id>',
type: RNWatchContextType.PLAYLIST, // PLAYLIST | WATCHLIST | SEASON
},
},
autoPlay: true,
});
};

Smart subtitles

Smart subtitles let you map preferred subtitle languages per audio track:

import { RNSubtitleLanguageType } from '@dicetechnology/react-native-vesper-sdk';

ref.current?.load({
source: {
id: '<video_id>',
isLive: false,
preferredSmartSubtitles: [
{
audioLanguage: { code: 'en' },
preferredSubtitleLanguages: [
{ code: 'en', kind: RNSubtitleLanguageType.CAPTIONS },
{ code: 'zh', kind: RNSubtitleLanguageType.SUBTITLES },
],
},
],
},
autoPlay: true,
});

Take control

ref.current?.play();
ref.current?.pause();
ref.current?.unload();
ref.current?.seekToPosition({ position: 100 }); // VOD — position in seconds
ref.current?.seekToTimestamp({ timestamp: unixTimestamp }); // LIVE only — Unix timestamp in seconds

Player configuration

Pass a playerConfig prop to configure player-level features:

import type { RNPlayerConfig } from '@dicetechnology/react-native-vesper-sdk';
import { RNBandwidthPolicyMode } from '@dicetechnology/react-native-vesper-sdk';

const playerConfig: RNPlayerConfig = {
toggles: {
isAirplayEnabled: true,
isChromecastEnabled: true,
isPipEnabled: true,
isLockScreenControlsEnabled: true,
isMuted: false,
},
bandwidthPolicy: {
mode: RNBandwidthPolicyMode.UNLIMITED, // UNLIMITED | DATA_SAVER | AUTO
threshold: 1_500_000,
},
muxConfig: {
playerName: 'My Player',
playerVersion: '1.0.0',
},
adsMacroHeaders: {
'CM-DVC-DNT': '0',
},
};

<RNPlayerView ref={ref} style={styles.box} playerConfig={playerConfig} />

View configuration

Pass a viewConfig prop to configure the player UI:

import type { RNViewConfig } from '@dicetechnology/react-native-vesper-sdk';
import { RNPlayerViewDisplayType } from '@dicetechnology/react-native-vesper-sdk';

const viewConfig: RNViewConfig = {
displayType: RNPlayerViewDisplayType.REGULAR, // MAX | REGULAR | MINI_PLAYER | MINI_BAR | NAKED
toggles: {
isDebug: false,
isAutoPlayNextEnabled: true,
isFullscreenButtonHidden: false,
isCloseButtonHidden: false,
isCloseButtonIconChevron: false,
isStatsButtonHidden: false,
isSettingsButtonHidden: false,
isShareButtonHidden: false,
isFavouriteButtonHidden: false,
isFavourite: false,
isWatchlistButtonHidden: false,
isEpgButtonHidden: false,
isAnnotationsButtonHidden: false,
},
nowPlaying: {
title: 'My Show',
channelLogoUrl: 'https://example.com/logo.png',
episodeInfo: 'S01E01',
startDate: Date.now() / 1000,
endDate: Date.now() / 1000 + 3600,
dateFormat: 'MMM d, yyyy',
},
};

<RNPlayerView ref={ref} style={styles.box} viewConfig={viewConfig} />

Listen

The SDK has an event-based system. Pass event callbacks as props to RNPlayerView:

import type {
RNPlayerStateChangedData,
RNFullScreenButtonTapData,
RNPlaybackErrorData,
RNBeaconErrorData,
RNHttpErrorData,
RNPlaybackPositionChangedData,
RNPlaybackRateChangedData,
RNTrackChangedData,
RNSeekTriggeredEventData,
RNSeekEndedEventData,
RNSkipMarkerData,
RNBandwidthPolicyModeChangedData,
RNVideoChangedData,
RNCastStatusChangedData,
RNPlaylistViewStateChangedData,
RNSmartSubtitlePreferenceChangedData,
} from '@dicetechnology/react-native-vesper-sdk';

export default function App() {
const ref = React.createRef<RNPlayerView>();

const handlePlayerStateChangedEvent = (data: RNPlayerStateChangedData) => {
console.info(`PlayerState: ${data.state}`);
};

const handleFullScreenButtonTappedEvent = (data: RNFullScreenButtonTapData) => {
if (data.displayType !== RNPlayerViewDisplayType.MAX) {
setDisplayType(RNPlayerViewDisplayType.MAX);
Orientation.lockToLandscapeLeft();
} else {
setDisplayType(RNPlayerViewDisplayType.REGULAR);
Orientation.lockToPortrait();
}
};

const handleCloseButtonTapEvent = () => {
ref.current?.unload();
};

const handlePlaybackErrorEvent = (data: RNPlaybackErrorData) => {
console.error(`Playback error: ${JSON.stringify(data)}`);
};

const handleBeaconErrorEvent = (data: RNBeaconErrorData) => {
console.error(`Beacon error: ${JSON.stringify(data)}`);
};

const handleHttpErrorEvent = (data: RNHttpErrorData) => {
console.error(`HTTP error: ${JSON.stringify(data)}`);
};

const handleEnterPipEvent = () => setIsPipActive(true);
const handleExitPipEvent = () => setIsPipActive(false);

const handlePlaybackPositionChangedEvent = (data: RNPlaybackPositionChangedData) => {
console.info(`Position: ${data.currentPosition}`);
};

const handlePlaybackRateChangedEvent = (data: RNPlaybackRateChangedData) => {
console.info(`Rate: ${data.rate}`);
};

const handleAudioTrackChangedEvent = (data: RNTrackChangedData) => {
console.info(`Audio track: ${data.language}`);
};

const handleSubtitleTrackChangedEvent = (data: RNTrackChangedData) => {
console.info(`Subtitle track: ${data.language}`);
};

const handleVideoChangedEvent = (data: RNVideoChangedData) => {
console.info(`Video changed: ${data.id}`);
};

const handleCastStatusChangedEvent = (data: RNCastStatusChangedData) => {
console.info(`Casting: ${data.isCasting}`);
};

return (
<View style={styles.container}>
<RNPlayerView
ref={ref}
style={styles.videoBox}
viewConfig={viewConfig}
playerConfig={playerConfig}
onPlayerStateChangedEvent={handlePlayerStateChangedEvent}
onFullScreenButtonTappedEvent={handleFullScreenButtonTappedEvent}
onCloseButtonTapEvent={handleCloseButtonTapEvent}
onPlaybackErrorEvent={handlePlaybackErrorEvent}
onBeaconErrorEvent={handleBeaconErrorEvent}
onHttpErrorEvent={handleHttpErrorEvent}
onEnterPipEvent={handleEnterPipEvent}
onExitPipEvent={handleExitPipEvent}
onPlaybackPositionChangedEvent={handlePlaybackPositionChangedEvent}
onPlaybackRateChangedEvent={handlePlaybackRateChangedEvent}
onAudioTrackChangedEvent={handleAudioTrackChangedEvent}
onSubtitleTrackChangedEvent={handleSubtitleTrackChangedEvent}
onVideoChangedEvent={handleVideoChangedEvent}
onCastStatusChangedEvent={handleCastStatusChangedEvent}
/>
</View>
);
}

All available events

Event Data type Description
onPlayerStateChangedEvent RNPlayerStateChangedData Player state transitions (LOADING, LOADED, PLAYING, PAUSED, BUFFERING, FAILED, ENDED)
onPlaybackPositionChangedEvent RNPlaybackPositionChangedData Periodic position updates
onPlaybackRateChangedEvent RNPlaybackRateChangedData Playback speed changed
onAudioTrackChangedEvent RNTrackChangedData Active audio track changed
onAudioTrackSelectedEvent RNTrackChangedData User selected an audio track
onSubtitleTrackChangedEvent RNTrackChangedData Active subtitle track changed
onSubtitleTrackSelectedEvent RNTrackChangedData User selected a subtitle track
onSmartSubtitlesPreferenceChangedEvent RNSmartSubtitlePreferenceChangedData Smart subtitle preference changed
onSeekTriggeredEvent RNSeekTriggeredEventData Seek initiated (contains seek type)
onSeekEndedEvent RNSeekEndedEventData Seek completed (contains start/end positions)
onSkipMarkerButtonTappedEvent RNSkipMarkerData Skip intro/credits button tapped
onBandwidthPolicyModeChangedEvent RNBandwidthPolicyModeChangedData Bandwidth policy mode changed
onVideoChangedEvent RNVideoChangedData Playing a different video (e.g. playlist advance)
onCastStatusChangedEvent RNCastStatusChangedData Cast session started or ended
onPlaylistViewStatedChangedEvent RNPlaylistViewStateChangedData Playlist panel opened or closed
onFullScreenButtonTappedEvent RNFullScreenButtonTapData Fullscreen button tapped
onCloseButtonTapEvent Close button tapped
onShareButtonTappedEvent Share button tapped
onFavouriteButtonTappedEvent Favourite button tapped
onWatchlistButtonTappedEvent Watchlist button tapped
onStatsButtonTappedEvent Stats button tapped
onEpgButtonTappedEvent EPG button tapped
onAnnotationsButtonTappedEvent Annotations button tapped
onCastButtonTappedEvent Cast button tapped
onSettingButtonTappedEvent Settings button tapped
onPlaybackErrorEvent RNPlaybackErrorData Playback error
onBeaconErrorEvent RNBeaconErrorData Beacon/analytics error
onHttpErrorEvent RNHttpErrorData HTTP/auth error
onEnterPipEvent Player entered PiP mode
onExitPipEvent Player exited PiP mode
onAdBreakStartedEvent Ad break started
onAdStartedEvent Individual ad started
onAdPausedEvent Ad paused
onAdResumedEvent Ad resumed
onAdEndedEvent Individual ad ended
onAdBreakEndedEvent Ad break ended
onAdLearnMoreTappedEvent Ad "Learn more" button tapped

Picture in Picture (PiP)

iOS

To make PiP work for your app you need to add corresponding Background Mode to your Xcode project. This will allow the player to automatically switch to PiP mode when the app is backgrounded.

Android

The SDK supports the picture-in-picture feature. To enable this, the hosting activity must declare support for the feature by adding android:supportsPictureInPicture="true" in the AndroidManifest.xml file:

<activity
android:name=".MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:supportsPictureInPicture="true" />

You also need to notify the SDK when to enter picture-in-picture mode and when the mode has changed via the VesperPlayerPipService. This is typically done within the following callbacks of the hosting activity:

import com.dice.vesper.rn.android.VesperPlayerPipService;

@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode);
VesperPlayerPipService.onPictureInPictureModeChanged(isInPictureInPictureMode);
}

@Override
protected void onUserLeaveHint() {
super.onUserLeaveHint();
VesperPlayerPipService.onUserLeaveHint();
}

Chromecast

iOS

Add NSBonjourServices to your Info.plist Specify NSBonjourServices in your Info.plist to allow local network discovery to succeed on iOS 14.

You will need to add both _googlecast._tcp and _<cast_receiver_id>._googlecast._tcp as services for device discovery to work properly.

Vesper SDK will parse Info.plist and configure Google cast context

Update the following example NSBonjourServices definition and replace ABCD1234 with your cast receiver id.

<key>NSBonjourServices</key>
<array>
<string>_googlecast._tcp</string>
<string>_ABCD1234._googlecast._tcp</string>
</array>

Add NSLocalNetworkUsageDescription to your Info.plist We strongly recommend that you customize the message shown in the Local Network prompt by adding an app-specific permission string in your app's Info.plist file for the NSLocalNetworkUsageDescription such as to describe Cast discovery and other discovery services, like DIAL.

<key>NSLocalNetworkUsageDescription</key>
<string>${PRODUCT_NAME} uses the local network to discover Cast-enabled devices on your WiFi
network.</string>
This message will appear as part of the iOS Local Network Access dialog as shown in the mock.

After this configurations are done casting with Vesper SDK is trivial. Once the player has been created and the content is loaded, all you need to do is tap on cast button in the right corner of player UI.

Android

Add the cast_receiver_id to your strings.xml resource:

<resources xmlns:tools="http://schemas.android.com/tools">
<string name="vesper_cast_receiver_id">ABCD1234</string>
</resources>

SharePlay (iOS only)

The SDK supports SharePlay via RNShareplayModule. Call startListeningForGroupSessions() once at startup, then assign a handler to onNewSession to respond when a group session arrives:

import { RNShareplayModule } from '@dicetechnology/react-native-vesper-sdk';
import type { RNNewSessionData } from '@dicetechnology/react-native-vesper-sdk';

// Start listening (call once, e.g. in your app root)
RNShareplayModule.startListeningForGroupSessions();

// Handle incoming sessions
RNShareplayModule.onNewSession = (data: RNNewSessionData) => {
ref.current?.load({
source: {
id: data.groupSessionStreamId,
isLive: data.isLive,
},
autoPlay: true,
});
};

Plugins

You can mount custom React Native components inside the player UI via the plugins field of viewConfig:

const viewConfig: RNViewConfig = {
plugins: {
overlays: {
side: {
buttonIconUrl: 'ic_info',
buttonTTSIdentifier: 'side_plugin',
component: {
height: 300,
width: 50,
name: 'SidePlugin', // registered RN component name
initialProps: { someProp: 'value' },
},
},
},
embedded: {
slideUp: {
height: 200,
width: 1920,
name: 'SlideUpPlugin', // registered RN component name
initialProps: { someProp: 'value' },
},
},
},
};