Install the package:
npm install react-native-cross-player
# or
yarn add react-native-cross-playerInstall the required peer dependencies (required by the library at runtime):
npm install --save nativewind react react-native react-native-awesome-slider react-native-blob-util react-native-gesture-handler react-native-orientation-locker react-native-reanimated react-native-safe-area-context react-native-svg react-native-system-navigation-bar react-native-video react-native-worklets tailwindcss
# or with yarn
yarn add nativewind react react-native react-native-awesome-slider react-native-blob-util react-native-gesture-handler react-native-orientation-locker react-native-reanimated react-native-safe-area-context react-native-svg react-native-system-navigation-bar react-native-video react-native-worklets tailwindcssNotes:
- You do not need to install these in a library project if your app already provides them; they are peers and must be available in the consuming app.
This library uses NativeWind v4 for styling. You must set up NativeWind in your consuming application.
For Web: Import the CSS file in your global CSS or directly in your app:
/* Option 1: In your global.css or app.css */
@import "react-native-cross-player/styles.css";Or import directly in your entry file (e.g., with Metro Web / Expo Web):
// In your App.tsx or _layout.tsx
import "react-native-cross-player/styles.css";For Native (iOS/Android): The styles are applied via NativeWind's className processing. Make sure your babel.config.js includes:
module.exports = function (api) {
api.cache(true);
return {
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
plugins: ['@babel/plugin-proposal-export-namespace-from', 'react-native-reanimated/plugin'],
};
};Your Expo Metro config should pass the same global CSS entry to NativeWind:
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: './global.css' });Basic usage in a React Native app:
import React from "react";
import { VideoPlayer } from "react-native-cross-player";
export default function App() {
const playerId = "demo-player";
const playerConfig = {
playerId,
videoSources: [
{
id: "main",
playerId,
label: "Main stream",
source: "https://example.com/video.m3u8",
format: "m3u8"
}
],
subtitleSources: [],
initialVideoSource: 0
};
return (
<VideoPlayer
videoTitle="Demo"
playerConfig={playerConfig}
viewStyle={{ flex: 1 }}
theme={{
minimumTrackTintColor: "#0ea5e9",
maximumTrackTintColor: "#3f3f46",
cacheTrackTintColor: "#71717a",
bubbleBackgroundColor: "#0ea5e9"
}}
/>
);
}You can also import the controls separately:
import { VideoPlayer, PlayerControls } from "react-native-cross-player";VideoPlayer is the ready-made player shell, but the package also exports the lower-level pieces used to build it. Use usePlayerController when you want your own layout, your own buttons, or a custom TV/mobile/web player surface while still reusing the package playback logic for HLS, subtitles, source switching, proxy handling, fullscreen, audio tracks, and progress state.
The custom-player flow is:
- Render your own
react-native-videoelement. - Pass
videoRef,controlsRef, andplayerViewRefintousePlayerController. - Spread
controller.nativeVideoPropsonto your video element. - Keep native video controls disabled and drive playback through
controller.controls. - Render your own UI, or reuse the exported
PlayerControlsoverlay.
import React from "react";
import { Pressable, Text, View } from "react-native";
import Video from "react-native-video";
import { PlayerControls, usePlayerController } from "react-native-cross-player";
const playerId = "custom-player";
export function CustomPlayer() {
const videoRef = React.useRef(null);
const controlsRef = React.useRef(null);
const playerViewRef = React.useRef(null);
const controller = usePlayerController({
playerId,
videoRef,
controlsRef,
playerViewRef,
videoSources: [
{
id: "main",
playerId,
label: "Main stream",
source: "https://tears-of-steel-subtitles.s3.amazonaws.com/tos.mp4",
format: "mp4"
}
],
subtitleSources: [],
initialVideoSource: 0
});
return (
<View ref={playerViewRef} style={{ flex: 1, backgroundColor: "black" }}>
<Video
ref={videoRef}
{...controller.nativeVideoProps}
controls={false}
paused={controller.playerState.paused}
resizeMode="contain"
style={{ flex: 1 }}
/>
<Pressable onPress={() => controller.controls.setPause(!controller.playerState.paused)}>
<Text>{controller.playerState.paused ? "Play" : "Pause"}</Text>
</Pressable>
<PlayerControls
ref={controlsRef}
videoTitle="Custom player"
controls={controller.controls}
resources={controller.playbackResources}
playerState={controller.playerState}
/>
</View>
);
}The package entry exports more than the ready-made components:
- UI:
VideoPlayer,PlayerControls,VideoPlayerRef,PlayerControlsRef,ControlsProps. - Controller hooks:
usePlayerController,PlayerControllerProps,useWebKeyboard. - Media helpers:
createM3U8Source,createMasterM3U8Raw,createVTTSource,convertSRTtoVTT,createM3U8File,createVTTFile,clearBlobFiles,clearBlobGroup,revokeAllBlobURLs. - HLS/proxy helpers:
HlsProxy,HlsProxyManager,ProxyLoader,ProxyPlaylistLoader,ProxyFragmentLoader,HlsProxyConfig,ProxyURLResolverCallback. - Types:
VideoSource,SubtitleSource,SourceRequestOptions,M3U8BlobOptions,M3U8PlaylistTrack,M38USubtitleTrack,M3U8AudioTrack,SubtitleBlobOptions,SourceTypes,SubtitleTypes,TextEncoding,VideoFormats. - Utilities:
CustomPlayerError,isCustomPlayerError,detectSourceType,detectSubtitleType,detectSubtitleEncoding,CNPLogger,ProxyLogger,CSS_PATH.
For the complete, structured API reference (component props, hook options, controllers and types) see API.md.
Why the API is exposed
This library exports both higher-level UI components (VideoPlayer, PlayerControls) and lower-level hooks/controllers (for example usePlayerController) so you can either use the ready-made player UI or build your own custom player interface. Exposing the controller makes it straightforward to wire the playback logic into a custom layout or use different control components while reusing the underlying HLS/subtitle/proxy logic.
usePlayerController is exported from the package entry so consumers can import it directly:
import { usePlayerController } from "react-native-cross-player";VideoPlayer is a small wrapper that wires usePlayerController and PlayerControls together. Important props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
videoTitle |
string |
— | Yes | Title displayed in the controls header |
language |
Languages |
en |
No | Localization setting |
playerConfig |
Omit<PlayerControllerProps, "playerViewRef" | "videoRef" | "controlsRef"> |
{} |
Yes | Full runtime configuration for usePlayerController. See key fields below. |
viewStyle |
StyleProp<ViewStyle> |
— | No | Style for the outer container view |
videoStyle |
StyleProp<ViewStyle> |
— | No | Style applied to the native video element |
theme |
SliderThemeType |
— | No | Optional slider theme forwarded to the built-in progress bar from react-native-awesome-slider |
onControlVisibilityChange |
(visible: boolean) => void |
— | No | Called whenever the built-in controls become visible or hidden |
onSourceChange |
(index: number, source: VideoSource) => void |
— | No | Called when the active video source changes, including initial source selection and imperative/UI-driven switches |
onSubtitleChange |
(index: number, subtitle: SubtitleSource) => void |
— | No | Called when a subtitle track becomes active. It is not fired when subtitles are turned off. |
onPlaybackChange |
(isPlaying: boolean) => void |
— | No | Called when playback toggles between playing and paused after the player has mounted. |
onProgress |
(currentTime: number) => void |
— | No | Called on each native progress update with the current playback time in seconds. |
onEnd |
() => void |
— | No | Called when playback reaches the end of the active media. |
theme lets you override the colors used by the built-in seek slider without replacing the controls UI.
import { VideoPlayer } from "react-native-cross-player";
import type { SliderThemeType } from "react-native-awesome-slider";
const sliderTheme: SliderThemeType = {
minimumTrackTintColor: "#0ea5e9",
maximumTrackTintColor: "#3f3f46",
cacheTrackTintColor: "#71717a",
bubbleBackgroundColor: "#0ea5e9"
};
<VideoPlayer videoTitle="Demo" playerConfig={playerConfig} theme={sliderTheme} />;You can observe source and subtitle switches from the built-in UI or imperative ref calls:
<VideoPlayer
videoTitle="Demo"
playerConfig={playerConfig}
onSourceChange={(index, source) => {
console.log("Active source", index, source.label);
}}
onSubtitleChange={(index, subtitle) => {
console.log("Active subtitle", index, subtitle.label ?? subtitle.langISO);
}}
onPlaybackChange={(isPlaying) => {
console.log("Playback changed", isPlaying ? "playing" : "paused");
}}
onProgress={(currentTime) => {
console.log("Current time", currentTime);
}}
onEnd={() => {
console.log("Playback finished");
}}
/>VideoPlayer also exposes an imperative ref API for playback and track/source switching.
type VideoPlayerRef = {
setState: (state: State) => void;
setSubtitle: (index: number) => Promise<void>;
setVideoSource: (index: number) => Promise<void>;
seek: (time: number) => void;
play: () => void;
pause: () => void;
getCurrentTime: () => Promise<number>;
getCurrentVideoIndex: () => number;
getCurrentSubtitleIndex: () => number;
};setSubtitle(index) selects a subtitle track by index, setVideoSource(index) switches the active video source by index, and the getter methods return the currently selected source and subtitle indexes.
Example usage (concise playerConfig):
const playerConfig = {
playerId: 'demo-player',
videoSources: [{
id: 'main',
playerId: 'demo-player',
label: 'Main stream',
source: 'https://example.com/stream.m3u8',
format: 'm3u8'
}],
subtitleSources: [],
initialVideoSource: -1,
autoStart: false,
proxyURL: 'https://proxy.example.com'
};
<VideoPlayer playerConfig={playerConfig} />Key playerConfig fields (examples):
| Field | Type | Default | Description |
|---|---|---|---|
videoSources |
VideoSource[] |
[] |
List of sources to present in the sources menu |
subtitleSources |
SubtitleSource[] |
[] |
List of subtitle tracks to offer |
initialVideoSource |
number |
-1 |
Index to auto-select a video source on mount |
initialSubtitleSource |
number |
-1 |
Index to auto-select a subtitle |
initialAudioTrack |
number |
-1 |
Index to auto-select an audio track (applied after load) |
autoStart |
boolean |
false |
Start playback automatically after load |
startPosition |
number |
0 |
Seek position (seconds) applied on initial load |
proxyURL |
string |
— | Proxy tunnel URL used for playlist and fragment requests |
lazyLoadSources |
boolean |
true |
Defer creation of blob/playlist URLs until first use |
preservePlaybackOnSourceChange |
boolean |
true |
Preserve current time when switching sources |
maxResolutionHeight |
number |
— | Prefer/filter quality levels with height <= this value. Useful to cap resolution for bandwidth or device constraints. |
For the full API (controller methods, control props, types and helper exports) see API.md.
Contributions are welcome. Open an issue or submit a PR. Follow the code style in the repo and add tests if you introduce behavior changes.
This project is licensed under the MIT License. See the full license text in LICENSE.