# 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](https://github.com/ThaUnknown/jassub)) and pgs (via [libpgs](https://github.com/Arcus92/libpgs-js))
- **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

```bash
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`:

```json
{
  "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:

<details>

<summary>ass rendering</summary>

#### 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.

</details>

## Quick start

Wrap your player UI in an `OmniProvider` with a `source`, then render an
`OmniView` and drive it with the hooks.

```tsx
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`  | x       | The media to play.                                   |
| `showNotification` | `boolean` | `false` | Create a media session (OS controls/notification).   |
| `children`         | `ReactNode` | x     | 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**

```ts
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**

```ts
player.currentTime = 42; // seek to absolute position (seconds)
player.playbackRate = 1.5;
player.volume = 0.8; // 0..1
player.muted = true;
```

**Readonly state**

```ts
player.status; // "idle" | "loading" | "readyToPlay" | "error"
player.isPlaying;
player.buffered; // seconds buffered
player.duration; // seconds
player.isAutoQuality; // whether rendition selection is automatic
```

**Tracks & renditions**

```ts
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.renditions; // Rendition[]
player.selectRendition(rendition); // pass undefined for automatic quality
```

> Prefer `usePlayerState` 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.

```ts
const status = usePlayerState("status");
const isPlaying = usePlayerState("isPlaying");
const duration = usePlayerState("duration");
const currentTime = usePlayerState("currentTime", 1); // poll every 1s

const audios = usePlayerState("audios"); // Track[]
const subtitles = usePlayerState("subtitles"); // Track[]
const renditions = usePlayerState("renditions"); // Rendition[]
```

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.

```ts
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.         |

The track/rendition change events fire when the _active_ selection changes;
`usePlayerState("audios" | "subtitles" | "renditions" | "videos")` exposes the
full lists as reactive state. Use whichever fits.

## Example app

A full example (playlist navigation, track/rendition selectors, an event log,
and web subtitle assets) lives in [`example/`](./example). To run it:

```bash
bun install
bun run android   # Android
bun run web       # Web
```
