react-native-omni
v1.2.2
Published
video player using vlc and videojs
Maintainers
Readme
react-native-omni
A library to have real players on android and web. It uses VLC on android and videojs v10 on the web (ios not implemented yet, PR welcome)
Features
- vlc v4: to support more codecs than exoplayer, hdr and so on
- Adaptive streaming: HLS out of the box, with automatic quality (rendition) selection or manual override.
- Multi-track playback: enumerate and switch video, audio, and subtitle tracks at runtime.
- Rich subtitle support: vtt, srt, ass (via jassub) and pgs (via libpgs)
- Picture-in-Picture: enter PiP automatically or on demand on Android.
- and basic player stuff: media sessions, playlists, hook based api...
Installation
bun add react-native-omni react-native-nitro-modulesExpo config plugin (Android)
The library ships an Expo config plugin that wires up media notifications and
picture-in-picture. Add it to your app.json / app.config.js:
{
"expo": {
"plugins": ["react-native-omni"]
}
}The plugin will:
- Register the
OmniPlayerServicemedia session service and add theFOREGROUND_SERVICE/FOREGROUND_SERVICE_MEDIA_PLAYBACKpermissions. - Enable Picture-in-Picture on your
MainActivity(declaressupportsPictureInPicture, adds the requiredconfigChanges, and hooks the pip lifecycle callbacks).
If you are not using Expo, replicate those manifest/activity changes manually.
Web setup
Most features are available out of the box, you need custom steps for advanced subtitles rendering:
Cross-origin isolation
jassub's wasm is multi-threaded and relies on SharedArrayBuffer, which
browsers only expose on a cross-origin-isolated page. Serve these response
headers on your HTML document:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentiallesscredentialless keeps cross-origin video/subtitle/font requests working without
requiring Cross-Origin-Resource-Policy headers on every remote asset. Without
isolation, JASSUB initializes but silently never renders.
Quick start
Wrap your player UI in an OmniProvider with a source, then render an
OmniView and drive it with the hooks.
import {
OmniProvider,
OmniView,
usePlayer,
usePlayerState,
useEvent,
} from "react-native-omni";
function Player() {
const player = usePlayer();
const isPlaying = usePlayerState("isPlaying");
const currentTime = usePlayerState("currentTime");
const duration = usePlayerState("duration");
useEvent("end", () => player.playNext());
return (
<>
<OmniView style={{ width: "100%", aspectRatio: 16 / 9 }} autoplay />
<Button
title={isPlaying ? "Pause" : "Play"}
onPress={() => (isPlaying ? player.pause() : player.play())}
/>
<Text>
{currentTime.toFixed(0)} / {duration.toFixed(0)}
</Text>
</>
);
}
export default function App() {
const source = {
src: [{ uri: "https://example.com/stream.m3u8", headers: {} }],
subtitles: [],
metadata: { title: "My video", hasPrev: false, hasNext: true },
};
return (
<OmniProvider source={source} showNotification>
<Player />
</OmniProvider>
);
}API
<OmniProvider>
Creates the underlying player and exposes it to children. Must wrap any
component that uses <OmniView>, usePlayer, usePlayerState, or useEvent.
| Prop | Type | Default | Description |
| ------------------ | --------- | ------- | ---------------------------------------------------- |
| source | Source | — | The media to play. |
| showNotification | boolean | false | Create a media session (OS controls/notification). |
| children | ReactNode | — | Your player UI. |
Updating source swaps the media in place (and seeks to source.startTime if
set) without recreating the player.
<OmniView>
Renders the video surface. Place it inside an OmniProvider. Accepts a style
plus:
| Prop | Type | Platform | Description |
| ---------------- | ---------------- | -------- | ----------------------------------------------------------------------- |
| autoplay | boolean | all | Start playing as soon as the media is ready. |
| autoPip | boolean | Android | Automatically enter Picture-in-Picture when the app is backgrounded. |
| subtitleAssets | SubtitleAssets | Web | URLs for the ASS/PGS renderer worker/wasm/font assets (see below). |
usePlayer()
Returns the OmniPlayer instance — an imperative handle for controlling
playback and reading/writing state.
Playback controls
player.play();
player.pause();
player.seekBy(offset); // relative seek in seconds
player.playPrev(); // fires the `prev` event (implement navigation yourself)
player.playNext(); // fires the `next` event
player.hasPrev; // readonly boolean
player.hasNext; // readonly booleanWritable state
player.currentTime = 42; // seek to absolute position (seconds)
player.playbackRate = 1.5;
player.volume = 0.8; // 0..1
player.muted = true;Readonly state
player.status; // "idle" | "loading" | "readyToPlay" | "error"
player.isPlaying;
player.buffered; // seconds buffered
player.duration; // seconds
player.isAutoQuality; // whether rendition selection is automaticTracks & renditions
player.videos; // Track[]
player.selectVideo(track);
player.audios; // Track[]
player.selectAudio(track);
player.subtitles; // Track[]
player.selectSubtitle(track); // pass undefined to turn subtitles off
player.rendition; // Rendition[]
player.selectRendition(rendition); // pass undefined for automatic qualityPrefer
usePlayerState/useEventfor values that change over time — reading them directly offplayergives you a one-time snapshot and won't re-render.
usePlayerState(key, refresh?)
Subscribes to a single reactive player property and re-renders when it changes. Only the component using it re-renders.
const status = usePlayerState("status");
const isPlaying = usePlayerState("isPlaying");
const duration = usePlayerState("duration");
const currentTime = usePlayerState("currentTime", 1); // poll every 1sThe optional refresh argument (seconds) sets a polling interval.
Useful for currentTime, which defaults to a 1s refresh (we don't refresh every
ms for this because crossing js/native bridge on native consumes lot of battery)
useEvent(event, callback)
Subscribes to a player event for the lifetime of the component.
useEvent("end", () => player.playNext());
useEvent("prev", handlePrev);
useEvent("next", handleNext);
useEvent("error", (type, message) => console.warn(type, message));
useEvent("audioFocusChange", (status) => {/* … */});
useEvent("videoTrackChange", (track) => {/* … */});
useEvent("audioTrackChange", (track) => {/* … */});
useEvent("subtitleChange", (track) => {/* track may be undefined = off */});
useEvent("renditionChange", (rendition) => {/* … */});| Event | Signature | When |
| ------------------ | -------------------------------------- | --------------------------------------------- |
| end | () => void | Playback reached the end. |
| prev / next | () => void | Prev/next requested (from UI or OS controls). |
| error | (type, message) => void | A playback error occurred. |
| audioFocusChange | (status) => void | The OS audio focus changed. |
| videoTrackChange | (track) => void | The active video track changed. |
| audioTrackChange | (track) => void | The active audio track changed. |
| subtitleChange | (track?) => void | The active subtitle changed (undefined = off). |
| renditionChange | (rendition) => void | The active quality/rendition changed. |
Example app
A full example (playlist navigation, track/rendition selectors, an event log,
and web subtitle assets) lives in example/. To run it:
bun install
bun run android # Android
bun run web # Web