Skip to content

Latest commit

 

History

37 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Orca Video Player

npm version license MIT build status lib React Native CLI support on Patreon

@orca-runtime/orca-video-player

React Native video player with disk caching, preloading, and multi-URI support. Published on npm under the @orca-runtime scope. Built on Nitro Modules.

Installation

npm install @orca-runtime/orca-video-player react-native-nitro-modules
yarn add @orca-runtime/orca-video-player react-native-nitro-modules
pnpm add @orca-runtime/orca-video-player react-native-nitro-modules

react-native-nitro-modules is a required peer dependency.

After installing or changing native code, run:

yarn nitrogen   # first time / after *.nitro.ts changes
cd ios && pod install

Orca Video Player comparison

Package exports

All public APIs are imported from @orca-runtime/orca-video-player:

import {
  OrcaVideoPlayer,
  OrcaVideoPlayerCacheApi,
  useVideoCache,
  getVideoUris,
  isRemoteVideoUri,
  resolveVideoSource,
  resolveVideoUri,
} from '@orca-runtime/orca-video-player';

import type {
  OrcaVideoPlayerProps,
  OrcaVideoPlayerHandle,
  VideoSource,
  VideoUri,
  VideoSize,
  ResizeMode,
  ResolvedVideoSource,
  UseVideoCacheOptions,
  UseVideoCacheResult,
} from '@orca-runtime/orca-video-player';

Quick start

import { OrcaVideoPlayer } from '@orca-runtime/orca-video-player';

<OrcaVideoPlayer
  source={{ uri: 'https://example.com/video.mp4' }}
  autoplay
  muted
  controls
  resizeMode="cover"
  onProgress={(time) => console.log(time)}
  onEnd={() => console.log('ended')}
/>;

Looping

Replay the same source indefinitely in a single player instance. When loop is enabled, onEnd is not called:

<OrcaVideoPlayer
  source={{ uri: 'https://example.com/video.mp4' }}
  loop
  autoplay
  muted
/>

Imperative control

Attach a ref to control playback from your own UI — useful when controls={false} and you need custom Play / Pause / Seek buttons:

import { useRef } from 'react';
import { Pressable, Text } from 'react-native';
import {
  OrcaVideoPlayer,
  type OrcaVideoPlayerHandle,
} from '@orca-runtime/orca-video-player';

const playerRef = useRef<OrcaVideoPlayerHandle>(null);

<>
  <OrcaVideoPlayer
    ref={playerRef}
    source={{ uri: 'https://example.com/video.mp4' }}
    controls={false}
    resizeMode="cover"
  />

  <Pressable onPress={() => playerRef.current?.play()}>
    <Text>Play</Text>
  </Pressable>
  <Pressable onPress={() => playerRef.current?.pause()}>
    <Text>Pause</Text>
  </Pressable>
  <Pressable onPress={() => playerRef.current?.seekTo(10)}>
    <Text>Seek to 10s</Text>
  </Pressable>
  <Pressable onPress={() => playerRef.current?.enterPictureInPicture()}>
    <Text>PiP</Text>
  </Pressable>
</>;

Ref methods

Method Description
play() Start or resume playback
pause() Pause playback
seekTo(seconds) Seek to a position in seconds
enterPictureInPicture() Enter Picture-in-Picture mode
exitPictureInPicture() Exit Picture-in-Picture mode
interface OrcaVideoPlayerHandle {
  play(): void;
  pause(): void;
  seekTo(seconds: number): void;
  enterPictureInPicture(): void;
  exitPictureInPicture(): void;
}

Ref methods are available on iOS, Android, and web. On web, play() may be blocked by the browser until the user interacts with the page.

Enable Picture-in-Picture

Enable PiP on a player and optionally enter it automatically when the app moves to the background:

const playerRef = useRef<OrcaVideoPlayerHandle>(null);

<OrcaVideoPlayer
  ref={playerRef}
  source={{ uri: 'https://example.com/video.mp4' }}
  allowsPictureInPicture
  autoEnterPictureInPicture
  onPictureInPictureChange={(active) => console.log('PiP active:', active)}
/>

<Pressable onPress={() => playerRef.current?.enterPictureInPicture()}>
  <Text>Enter PiP</Text>
</Pressable>

Platform setup

iOS — add background audio mode to your app's Info.plist:

<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
</array>

When controls={true} and allowsPictureInPicture is enabled, the system controls may also show a PiP button.

Android — PiP requires host app configuration:

  1. Enable PiP on your main activity in AndroidManifest.xml:
<activity
  android:name=".MainActivity"
  android:supportsPictureInPicture="true"
  ...
/>
  1. Forward lifecycle hooks from MainActivity:
import com.margelo.nitro.orcavideoplayer.OrcaVideoPlayerPipHelper

override fun onUserLeaveHint() {
  OrcaVideoPlayerPipHelper.onUserLeaveHint(this)
  super.onUserLeaveHint()
}

override fun onPictureInPictureModeChanged(
  isInPictureInPictureMode: Boolean,
  newConfig: Configuration,
) {
  super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
  OrcaVideoPlayerPipHelper.onPictureInPictureModeChanged(isInPictureInPictureMode)
}

autoEnterPictureInPicture uses onUserLeaveHint to enter PiP when the user leaves the app while video is playing.

Web — uses the Document Picture-in-Picture API. Auto PiP on tab hide works only in browsers that support it; otherwise it is a no-op. User interaction may be required before enterPictureInPicture() succeeds.

PiP is best tested on a physical iOS device. The iOS Simulator has limited PiP support.

OrcaVideoPlayer props

Prop Type Default Description
source VideoSource | number required Video source (URI object or bundled require() asset id)
uriIndex number 0 Index into source.uri when it is an array
autoplay boolean false Start playback automatically
muted boolean false Mute audio
controls boolean false Show native playback controls
resizeMode 'cover' | 'contain' | 'stretch' 'contain' How video fills the view
preload boolean false Buffer the video in the player without starting playback (unless autoplay is also set)
lowMemory boolean false Minimize buffer/decoder memory for concurrent playback (see Low-memory mode)
loop boolean false Replay the same source indefinitely. When true, onEnd is not called
allowsPictureInPicture boolean false Enable Picture-in-Picture support
autoEnterPictureInPicture boolean false Enter PiP automatically when the app moves to the background (requires platform setup)
width number Container width (merged into style, overrides style.width)
height number Container height (merged into style, overrides style.height)
onProgress (time: number) => void Called with current time in seconds
onEnd () => void Called when playback finishes (not called when loop is true)
onPictureInPictureChange (active: boolean) => void Called when PiP mode starts or stops
onVideoSizeChange (size: VideoSize) => void Called with natural video dimensions as soon as metadata is loaded
style StyleProp<ViewStyle> Container style

Video size

onVideoSizeChange fires when the video's natural dimensions become available (on load), without waiting for playback to start. It may fire again if the resolution changes (for example adaptive streams).

<OrcaVideoPlayer
  source={{ uri: 'https://example.com/video.mp4' }}
  width={360}
  height={220}
  onVideoSizeChange={({ width, height }) => {
    console.log('natural size', width, height);
  }}
/>
interface VideoSize {
  width: number;
  height: number;
}

VideoSource

type VideoUri = string | number;

interface VideoSource {
  uri: VideoUri | VideoUri[];
  headers?: Record<string, string>;
  type?: string;
}

uri accepts a remote URL string or a Metro asset id from require('./video.mp4'). You can also pass the asset id directly as source:

// Bundled MP4 via require
<OrcaVideoPlayer source={{ uri: require('./video.mp4') }} autoplay muted />

// Equivalent bare asset source
<OrcaVideoPlayer source={require('./video.mp4')} autoplay muted />

Bundled / file:// URIs are played as-is. Disk cache preload only runs for http:// and https:// URLs.

Multiple URIs

Pass an array of remote URLs and select which one to play with uriIndex:

import { useState } from 'react';
import { OrcaVideoPlayer } from '@orca-runtime/orca-video-player';

const VIDEOS = [
  'https://example.com/video-a.mp4',
  'https://example.com/video-b.mp4',
];

const [index, setIndex] = useState(0);

<OrcaVideoPlayer
  source={{ uri: VIDEOS }}
  uriIndex={index}
  controls
  resizeMode="cover"
/>;

Player preload vs disk cache

There are two different preload mechanisms:

Mechanism API Scope Use case
Player preload OrcaVideoPlayer preload prop In-memory, tied to mounted player Buffer early on the same screen
Disk cache OrcaVideoPlayerCacheApi / useVideoCache Persistent on device, global Preload at app start, play on another screen

Player preload (in-memory)

Prepares the native player and buffers media without starting playback:

<OrcaVideoPlayer
  source={{ uri: 'https://example.com/video.mp4' }}
  preload
  autoplay={false}
  controls
/>

When the player unmounts, this buffer is released. For cross-screen preloading, use the disk cache API below.

Compare preload modes side by side

<OrcaVideoPlayer
  source={{ uri: 'https://example.com/video.mp4' }}
  preload
  autoplay={false}
  controls
/>

<OrcaVideoPlayer
  source={{ uri: 'https://example.com/video.mp4' }}
  preload={false}
  autoplay={false}
  controls
/>;

Low-memory mode

Use lowMemory when several players may be mounted at once (feeds, grids). It does not re-encode video or switch to a lower-resolution URI — progressive files still decode at their encoded resolution. Instead it reduces how much each player keeps buffered and, on Android, how many players may hold an active decoder at the same time.

<OrcaVideoPlayer
  source={{ uri: 'https://example.com/video.mp4' }}
  lowMemory
  autoplay
  muted
  loop
/>
Platform Behavior when lowMemory is enabled
Android Small ExoPlayer LoadControl buffers; at most 2 active prepared players; others stay idle until play() / autoplay / preload
iOS Short preferredForwardBufferDuration, lower preferredPeakBitRate, view-based preferredMaximumResolution
Web Uses preload="metadata" when not autoplaying

Feed tips:

  • Set lowMemory on list/grid cells.
  • Prefer pause() or unmount for off-screen cells so Android can free decoder slots.
  • For true lower visual quality you still need a smaller source URI (or adaptive streams); this mode only caps buffering and concurrency.

Disk cache API

OrcaVideoPlayerCacheApi is a global native singleton. It downloads videos to the device cache directory and resolves them to a local file:// URI.

import { OrcaVideoPlayerCacheApi } from '@orca-runtime/orca-video-player';

// Download one video
await OrcaVideoPlayerCacheApi.preload({
  uri: 'https://example.com/video.mp4',
});

// Download multiple videos
await OrcaVideoPlayerCacheApi.preload({
  uri: ['https://example.com/video-a.mp4', 'https://example.com/video-b.mp4'],
});

// Check cache status
OrcaVideoPlayerCacheApi.isCached('https://example.com/video.mp4'); // boolean

// Get local file URI (null if not cached yet)
const localUri = OrcaVideoPlayerCacheApi.getCachedUri(
  'https://example.com/video.mp4'
);

// Clear cache
await OrcaVideoPlayerCacheApi.clearCache('https://example.com/video.mp4');
await OrcaVideoPlayerCacheApi.clearAllCache();

Play from cache

const remoteUri = 'https://example.com/video.mp4';
const cachedUri = OrcaVideoPlayerCacheApi.getCachedUri(remoteUri);

<OrcaVideoPlayer source={{ uri: cachedUri ?? remoteUri }} autoplay controls />;

useVideoCache hook

React hook that wraps the disk cache API and returns a resolved source for OrcaVideoPlayer.

import {
  OrcaVideoPlayer,
  useVideoCache,
} from '@orca-runtime/orca-video-player';

const { source, isCached, isPreloading, preload, clearCache } = useVideoCache(
  { uri: 'https://example.com/video.mp4' },
  {
    autoPreload: false, // download on mount
    preferCache: true, // use local file URI when available
    uriIndex: 0, // active URI when uri is an array
  }
);

<OrcaVideoPlayer source={source} controls />;

Hook options

Option Type Default Description
autoPreload boolean false Download to disk when the hook mounts
preferCache boolean true Resolve source.uri to a cached local URI
uriIndex number 0 Active URI when source.uri is an array

Hook return value

Field Type Description
source VideoSource Resolved source (local URI when cached)
uris string[] All remote URIs
activeUri string Currently selected remote URI
isCached boolean true when all URIs are cached
cachedByUri Record<string, boolean> Per-URI cache status
isPreloading boolean Download in progress
error Error | null Last preload error
preload () => Promise<void> Manually trigger download
clearCache () => Promise<void> Clear cache for all URIs in the source

Multiple URIs with the hook

import { useState } from 'react';
import {
  OrcaVideoPlayer,
  useVideoCache,
} from '@orca-runtime/orca-video-player';

const [uriIndex, setUriIndex] = useState(0);

const { source, cachedByUri, preload } = useVideoCache(
  { uri: ['https://example.com/a.mp4', 'https://example.com/b.mp4'] },
  { uriIndex, preferCache: true }
);

// cachedByUri['https://example.com/a.mp4'] → boolean

<OrcaVideoPlayer source={source} uriIndex={uriIndex} controls />;

Auto-preload on mount

const { source } = useVideoCache(
  { uri: 'https://example.com/video.mp4' },
  { autoPreload: true, preferCache: true }
);

Preload at app start, play on another screen

Recommended pattern for feed / detail flows:

// Root layout / app bootstrap
import { useEffect } from 'react';
import { OrcaVideoPlayerCacheApi } from '@orca-runtime/orca-video-player';

const FEED_VIDEOS = [
  'https://example.com/video-a.mp4',
  'https://example.com/video-b.mp4',
];

export function AppBootstrap() {
  useEffect(() => {
    OrcaVideoPlayerCacheApi.preload({ uri: FEED_VIDEOS });
  }, []);

  return <Navigation />;
}
// Player screen (different route)
import {
  OrcaVideoPlayer,
  useVideoCache,
} from '@orca-runtime/orca-video-player';

export function PlayerScreen({ remoteUri }: { remoteUri: string }) {
  const { source } = useVideoCache({ uri: remoteUri }, { preferCache: true });

  return <OrcaVideoPlayer source={source} autoplay controls />;
}

If the user opens the player before preload finishes, preferCache falls back to the remote URL automatically.

Utility helpers

import {
  getVideoUris,
  isRemoteVideoUri,
  resolveVideoSource,
  resolveVideoUri,
} from '@orca-runtime/orca-video-player';

getVideoUris(['https://a.mp4', 'https://b.mp4']);
// → ['https://a.mp4', 'https://b.mp4']

getVideoUris('https://a.mp4');
// → ['https://a.mp4']

resolveVideoSource({ uri: ['https://a.mp4', 'https://b.mp4'] }, 1);
// → { uri: 'https://b.mp4' }

resolveVideoSource({ uri: require('./video.mp4') });
// → { uri: 'file:///.../video.mp4' } (or Metro asset HTTP URL in dev)

resolveVideoSource(require('./video.mp4'));
// → { uri: 'file:///.../video.mp4' }

isRemoteVideoUri('https://a.mp4'); // true
isRemoteVideoUri('file:///path/video.mp4'); // false

Platform notes

  • iOS: Cache stored in Caches/orca-video-cache/. Player preload uses AVPlayer buffering. lowMemory shortens forward buffer and applies peak bitrate / max resolution hints.
  • Android: Cache stored in cacheDir/orca-video-cache/. Player preload uses ExoPlayer prepare(). lowMemory uses a small LoadControl and caps active players at 2.
  • Web: Cache uses in-memory blob URLs (not persisted across page reloads). lowMemory prefers preload="metadata".
  • Cache may be evicted by the OS at any time. Always fall back to the remote URI.
  • Authenticated URLs (headers) are not yet supported by the native disk cache downloader.

Support

If you find this library useful, you can support Orca Runtime on Patreon.

Contributing

License

MIT


Made with create-react-native-library

About

React Native video player with disk caching, preloading, PiP, and multi-URI support

Topics

Resources

Code of conduct

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages