2026-07-28 19:47:50 +02:00
2026-05-15 21:34:10 +02:00
2026-07-14 14:50:02 +02:00
2026-04-06 12:53:56 +02:00
2026-04-19 21:05:55 +02:00
2026-04-06 12:53:56 +02:00
2026-04-06 12:53:56 +02:00
2026-04-19 23:40:54 +02:00
2026-07-26 20:42:47 +02:00
2026-04-19 15:39:49 +02:00

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.
  • Chromecast: cast to a receiver on both web and Android .
  • and basic player stuff: media sessions, playlists, hook based api...

Installation

bun add react-native-omni react-native-nitro-modules

Expo 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 OmniPlayerService media session service and add the FOREGROUND_SERVICE / FOREGROUND_SERVICE_MEDIA_PLAYBACK permissions.
  • Enable Picture-in-Picture on your MainActivity (declares supportsPictureInPicture, adds the required configChanges, and hooks the pip lifecycle callbacks).
  • Register omni's Chromecast OptionsProvider

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:

ass 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: credentialless

credentialless 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 boolean

Writable 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 automatic

Tracks & 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 quality

Prefer usePlayerState/useEvent for values that change over time — reading them directly off player gives 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 1s

The 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
S
Description
No description provided
Readme MIT
2.1 MiB
Languages
Kotlin 58.9%
TypeScript 34.8%
JavaScript 3.4%
Ruby 0.8%
Swift 0.7%
Other 1.4%