npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

spalla-react-native

v2.1.1

Published

Spalla SDK for RN

Readme

spalla-react-native

Spalla SDK for RN

Installation

npm install spalla-react-native react-native-video @react-native-async-storage/async-storage react-native-uuid react-native-google-cast

The player UI is implemented in JS on top of react-native-video (the THEOplayer maven repository is no longer needed). Telemetry and CDN failover ship as a precompiled native core (C++ TurboModule), so the package requires React Native 0.76+ with the New Architecture enabled and does not run in Expo Go (use a dev client / prebuild).

Required: apply the native patches

This SDK depends on fixes to react-native-video that are not upstream yet (DAI stream initialisation on Android, IMA ad overlay swallowing taps and a layout loop on iOS). They ship with the package and are applied by patch-package.

Add the postinstall hook to your app's package.json and reinstall:

{
  "scripts": {
    "postinstall": "spalla-apply-patches"
  }
}

Then rebuild the native app — patching node_modules alone does not change an already-compiled binary.

Verify at any time:

npx spalla-doctor

Without the patches, ad breaks and DAI live streams fail in ways that look like content or CDN problems. The SDK also detects it at runtime: it logs an error, emits an integrationWarning player event and exposes checkIntegration() for your own startup health check.

The patch is generated against react-native-video 6.19.2 exactly, so pin it ("react-native-video": "6.19.2") — a range like ^6.19.1 can resolve to a version the patch no longer applies to.

Recent npm versions warn that spalla-react-native has an unapproved install script (install-scripts not yet covered by allowScripts). It is safe to ignore: the patches are applied by the postinstall of your app, which always runs. npx spalla-doctor confirms it.

Expo users: this works with prebuild/CNG and EAS Build. If your build cache skips postinstall, run npx spalla-doctor in a build step to fail early.

iOS

Enable Google IMA ads support in your ios/Podfile before use_react_native!:

$RNVideoUseGoogleIMA = true

Then run pod install.

Android

Enable IMA and HLS support in android/build.gradle (buildscript.ext):

ext {
  // ...
  useExoplayerIMA = true
  useExoplayerHls = true
}

If you are migrating from a previous version, remove maven { url 'https://maven.theoplayer.com/releases' } from your repositories.

For Picture-in-Picture, keep android:supportsPictureInPicture="true" on your MainActivity in AndroidManifest.xml (the Expo plugin does this automatically).

Usage

import SpallaPlayer, { initialize } from 'spalla-react-native';

// make sure to call initialize as soon as possible on your app. Can be on top of index.js or App.js
initialize(
  'your spalla token',
  null //application id for chromecast.
);

// ...

const playerRef = React.useRef<SpallaPlayer | null>(null);

const [muted, setMuted] = React.useState(false);
const [playing, setPlaying] = React.useState(true);
const [subtitle, setSubtitle] = React.useState<String | null>('pt-br');

<SafeAreaView style={styles.container}>
  <SpallaPlayer
    ref={playerRef}
    style={styles.videoPlayer}
    contentId="Spalla contentId"
    muted={muted}
    hideUI={false}
    subtitle={subtitle}
    pipEnabled={false}
    onPlayerEvent={({ nativeEvent }) => {
      switch (nativeEvent.event) {
        case 'timeUpdate':
          console.log('timeupdate', nativeEvent.time);
          break;
        case 'durationUpdate':
          console.log('durationUpdate', nativeEvent.duration);
          break;
        case 'play':
        case 'playing':
          setPlaying(true);
          break;
        case 'pause':
          setPlaying(false);
          break;
        case 'muted':
          setMuted(true);
          break;
        case 'unmuted':
          setMuted(false);
          break;
        case 'subtitleSelected':
          console.log('subtitleSelected', nativeEvent.subtitle);
          setSubtitle(nativeEvent.subtitle);
          break;
        case 'subtitlesAvailable':
          console.log('subtitlesAvailable', nativeEvent.subtitles);
          break;
        case 'playbackRateSelected':
          setPlaybackRate(nativeEvent.rate);
          break;
        case 'metadataLoaded':
          console.log(
            'metadataLoaded',
            nativeEvent.isLive,
            nativeEvent.duration
          );
          break;
        default:
          console.log('event', nativeEvent.event);
      }
    }}
  >
  <View style={styles.uicontainer}>{/*Place your custom UI here*/}</View>
  </SpallaPlayer>
  <Button
    onPress={() => {
      if (playing) {
        playerRef.current?.pause();
      } else {
        playerRef.current?.play();
     }
    }}
    title={playing ? 'Pause' : 'Play'}
  />
  <Button
    onPress={() => setMuted(!muted)}
    title={muted ? 'Unmute' : 'Mute'}
  />
</SafeAreaView>

Props

| Property | Type | Description | | :----------------- | :------: | :----------- | | contentId | string | Spalla contentId that will be played | hideUI | boolean | hide or show the default UI (its a prop, but it can only be set once) | muted | boolean | mute/unmute video | startTime | number | time to start the video in seconds (defaults to 0 = start of the video) | onPlayerEvent| callback | Function that will be called with player events | subtitle | string | subtitle to enable (language code from subtitlesAvailable). Null will hide subtitles | audioTrack | string | audio track to enable (name/language from audioTracksAvailable). Null uses the default track | playbackRate | number | Playback speed. Allowed values are 0.5, 1.0, 1.5 and 2.0 | aspectRatio | string | how video fits the screen. "fit" | "fill" | "aspectFill" (can only be set once) | customImaParams | Map | Custom parameters for IMA ads. Key and value must be strings | customAds | Array | Custom VAST/VMAP ads. Must be an array of objects like {["url": "vast url", "offset": "start"]} | pipEnabled | boolean | enable PiP support. Remember to add android:supportsPictureInPicture="true" on MainActivity on Manifest.xml on Android, and add backgroundMode PiP on iOS

Imperative Methods

Control and inspect the player through its ref:

| Method | Description | | :----------------- | :----------- | | play() | Resume playback | pause() | Pause playback | seekTo(time) | Seek to a time (in seconds) | seekToLive() | Jump back to the live edge (live streams with DVR) | setSubtitle(lang \| null) | Select a subtitle by language/name; null disables subtitles | getSubtitle() | Currently selected subtitle (null when disabled) | getAvailableSubtitles() | Subtitle languages/names announced by subtitlesAvailable | setAudioTrack(track \| null) | Select an audio track by name/language; null returns to the default | getAudioTrack() | Currently selected audio track (falls back to the active track reported by the player) | getAvailableAudioTracks() | Audio tracks announced by audioTracksAvailable | setPlaybackRate(rate) | Set playback speed (one of getAvailablePlaybackRates()) | getPlaybackRate() | Current playback speed | getAvailablePlaybackRates() | [0.25, 0.5, 1.0, 1.25, 1.5, 2.0] | setBitrate(bitrate \| null) | Pin the quality to a variant bitrate (bps); null returns to automatic ABR | getBitrate() | Bitrate currently playing (bps), when known | getAvailableBitrates() | Variant bitrates from the HLS master and the player's video tracks | enterFullscreen() / exitFullscreen() | Toggle the native fullscreen player | isFullscreen() | Whether the native fullscreen player is presented | enterPiP() / exitPiP() | Toggle Picture-in-Picture (requires pipEnabled) | isInPiP() | Whether Picture-in-Picture is active | checkIntegration() | { ok, issue?, message? } — whether the required react-native-video patches are active (see Installation)

Invalid arguments (NaN/negative seeks, unknown playback rates, non-positive bitrates) are rejected and reported through telemetry instead of reaching the native player.

Setters coexist with the equivalent props (subtitle, audioTrack, playbackRate): the most recent change wins — calling a setter overrides the prop until the prop value changes again.

playerRef.current?.play();
playerRef.current?.pause();
playerRef.current?.seekTo(12); //position in seconds, if higher than duration it will move to the end
playerRef.current?.seekToLive();

// subtitles / audio
const subtitles = playerRef.current?.getAvailableSubtitles(); // e.g. ['pt-br', 'es']
playerRef.current?.setSubtitle(subtitles?.[0] ?? null);
playerRef.current?.setSubtitle(null); // disable
const tracks = playerRef.current?.getAvailableAudioTracks(); // e.g. ['Português', 'Español']
playerRef.current?.setAudioTrack(tracks?.[1] ?? null);

// speed, quality, fullscreen, PiP
playerRef.current?.setPlaybackRate(1.5);
const bitrates = playerRef.current?.getAvailableBitrates(); // e.g. [560301, 1136555, 1928717]
playerRef.current?.setBitrate(bitrates?.[0] ?? null); // pin lowest quality
playerRef.current?.setBitrate(null); // back to automatic ABR
playerRef.current?.enterFullscreen();
playerRef.current?.enterPiP();

On Android the pin selects the exact variant when the player has enumerated its video tracks; on iOS (and before tracks are known) it is applied as a preferredPeakBitRate cap, so the ABR picks the highest variant at or below the requested bitrate.

Player events

All events arrive through onPlayerEvent as { nativeEvent }:

| Event | Payload | Description | | :----------------- | :------: | :----------- | | play / pause / playing / buffering / ended | — | playback state changes | muted / unmuted | — | mute state changes | timeUpdate | time, seekableDuration | current position; seekableDuration is the end of the seekable range (DVR window on live) | durationUpdate | duration | content duration in seconds | metadataLoaded | isLive, duration, isVertical, dvrEnabled | stream metadata. With a pre-roll it fires as soon as the ad break starts (from the stream config) and again with the real dimensions once the content loads, so your app never waits on an ad to call play() | subtitlesAvailable | subtitles: string[] | subtitle languages/names available for subtitle | subtitleSelected | subtitle | echoes the subtitle prop | audioTracksAvailable | audioTracks: string[] | audio track names available for audioTrack | audioTrackSelected | audioTrack | echoes the audioTrack prop | thumbnailsAvailable | thumbnails: ThumbnailCue[] | scrubbing preview cues (see Thumbnails) | playbackRateSelected | rate | echoes the playback rate | enterPiP / exitPiP | — | Picture-in-Picture transitions | onEnterFullScreen / onExitFullScreen | — | fullscreen transitions | adBreakBegin / adBreakEnd / adBegin / adEnd / adError | — | ad lifecycle | integrationWarning | code, message | the required react-native-video patches are missing, outdated or not in the native build (see Installation) | error | message, canRetry | playback or loading failure

Subtitles

Subtitle languages come from the stream config (sp_player_legendas_idiomas) and/or from tracks embedded in the HLS manifest. Both are merged and announced via subtitlesAvailable; select one with the subtitle prop.

  • Embedded manifest tracks (recommended): rendered natively by AVPlayer/ExoPlayer, including native fullscreen and PiP.
  • Sidecar .srt fallback: for streams without embedded tracks the SDK downloads and parses the .srt and renders the cues as a JS overlay on both platforms (react-native-video cannot sideload subtitles into HLS on either OS). The overlay is not visible inside native fullscreen or PiP.

To migrate a VOD to embedded subtitles on the packaging side, use scripts/embed-subtitles.sh: it converts each <lang>.srt to WebVTT, generates one subtitle media playlist per language and rewrites the master playlist (EXT-X-MEDIA + SUBTITLES group). Run scripts/embed-subtitles.sh --help for options.

Live DVR

When a live stream has DVR enabled, metadataLoaded.dvrEnabled is true and every timeUpdate carries seekableDuration (the DVR window). Use seekTo(time) to scrub inside the window and seekToLive() to return to the live edge.

Thumbnails (scrubbing preview)

Thumbnails are resolved automatically for VoDs, in this order:

  1. EXT-X-IMAGE-STREAM-INF declared in the HLS master (Roku Image Media Playlist extension): the SDK parses the image playlist (EXT-X-TILES sprite grids or plain image segments) into cues.
  2. thumbnail.vtt convention next to the manifest (<CDN>/vod/<id>/thumbnail.vtt).

Either source emits thumbnailsAvailable with the same cue shape:

type ThumbnailCue = {
  start: number; // seconds
  end: number;
  uri: string; // sprite image URL (signed)
  x?: number; // sprite crop rect, present for #xywh storyboards
  y?: number;
  width?: number;
  height?: number;
};

Render the preview by cropping the sprite (an Image inside an overflow: 'hidden' container offset by -x/-y). See example/src/CustomControls.tsx for a complete scrub bar with previews.

EXT-X-I-FRAME-STREAM-INF renditions require no SDK handling: AVPlayer consumes them natively for trick play while scrubbing with the iOS native controls, and both players ignore them otherwise.

Chromecast

Chromecast support is provided through react-native-google-cast. Install it as shown above, then:

// Add the application id for chromecast. In this example, A123456
initialize(
  'your spalla token',
  'A123456'
);

Note: the application id passed to initialize() does not configure the native Cast discovery. The effective receiver App ID is the one set natively below (AppDelegate/Info.plist on iOS, AndroidManifest on Android), and changing it requires a native rebuild.

On iOS, initialize the cast context in your AppDelegate (didFinishLaunchingWithOptions), using your App ID:

#import <GoogleCast/GoogleCast.h>

GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc] initWithApplicationID:@"A123456"];
GCKCastOptions *options = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria];
// Without this, "Stop casting" only disconnects and the receiver keeps playing.
options.stopReceiverApplicationWhenEndingSession = YES;
[GCKCastContext setSharedInstanceWithOptions:options];

Also open info.plist as SourceCode and copy this inside the main dict. Make sure to change A123456 with your App ID (keep the underscore at the start). More details on this link if needed.

<key>NSBonjourServices</key>
<array>
  <string>_googlecast._tcp</string>
  <string>_A123456._googlecast._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>We need network access to search for Cast devices</string>

On Android, open Manifest.xml and add these meta data tags inside the tag (same level as activities). As before, change A123456 with your App ID.

<meta-data
    android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
    android:value="com.reactnative.googlecast.GoogleCastOptionsProvider"/>
<meta-data
    android:name="com.reactnative.googlecast.RECEIVER_APPLICATION_ID"
    android:value="A123456"/>

Spalla provides a RN View that you can use to add the cast button to your interface. Check the example app if you need an example of usage

import { SpallaCastButton } from 'spalla-react-native';

[...]

return <SpallaCastButton tintColor="white" />

Contributing

See the contributing guide to learn how to contribute to the repository and the development workflow.

License

MIT


Made with create-react-native-library