https://github.com/noma4i/nitro-player
Lightweight video player for React Native with HLS caching
https://github.com/noma4i/nitro-player
cache hls nitro react-native video-player
Last synced: 30 days ago
JSON representation
Lightweight video player for React Native with HLS caching
- Host: GitHub
- URL: https://github.com/noma4i/nitro-player
- Owner: noma4i
- License: mit
- Created: 2026-03-17T11:17:43.000Z (4 months ago)
- Default Branch: master
- Last Pushed: 2026-04-01T12:00:00.000Z (3 months ago)
- Last Synced: 2026-04-03T02:59:21.791Z (3 months ago)
- Topics: cache, hls, nitro, react-native, video-player
- Language: Kotlin
- Homepage:
- Size: 1.74 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# NitroPlay
Native-first video player for React Native with shared HLS transport, stream cache, and first-frame preview utilities.
## Installation
```sh
yarn add @noma4i/nitro-play react-native-nitro-modules
```
```sh
npm install @noma4i/nitro-play react-native-nitro-modules
```
```sh
pnpm add @noma4i/nitro-play react-native-nitro-modules
```
`react-native-nitro-modules >= 0.35.0` is a required peer dependency. React and React Native come from your app.
### iOS
```sh
cd ios && pod install
```
### Android
No extra steps - standard React Native 0.77+ autolinking handles the module.
### Installing a specific tag from GitHub
If you need a tag or branch before it is published to npm:
```sh
yarn add @noma4i/nitro-play@github:noma4i/nitro-player#v1.1.0-beta.1
```
## Quick Start
```tsx
import React from 'react';
import { NitroPlayerView } from '@noma4i/nitro-play';
export function FeedCard() {
return (
);
}
```
## Core API
| Surface | Purpose |
| ----------------------------------- | ------------------------------------------------------------------ |
| `NitroPlayerView` | Declarative native view with fullscreen, attach, and event props |
| `NitroPlayer` | Imperative player with properties, methods, and `addEventListener` |
| `createNitroSource(config)` | URI normalization and hybrid source factory |
| `streamCache` | Prefetch, header-aware stats, cache clear |
| `videoPreview` | Generated and cache-only first-frame lookup |
| `usePlaybackState(player)` | Subscribes to `onPlaybackState` and returns latest snapshot |
| `useEvent(player, event, listener)` | Managed event subscription helper |
Deep-dive reference lives in [docs/player-api.md](docs/player-api.md).
## Source DSL
Top-level `NitroSourceConfig` fields:
| Field | Type | Purpose |
| ----------- | ---------------------------- | --------------------------------------------------------------------------------- |
| `uri` | `string \| number` | Network URL, local `file://` URI, absolute local file path, or React Native asset |
| `headers` | `Record` | Request headers (part of cache/preview identity) |
| `metadata` | `NitroSourceMetadata` | Player-facing media metadata |
| `startup` | `'eager' \| 'lazy'` | Startup strategy (default `eager`) |
| `buffer` | `BufferConfig` | Explicit buffering policy |
| `retention` | `NitroSourceRetentionConfig` | Preload, offscreen retention, trim, feed eligibility |
| `transport` | `NitroSourceTransportConfig` | Transport routing policy |
| `preview` | `NitroSourcePreviewConfig` | First-frame generation policy |
### `NitroSourceMetadata`
All fields optional.
| Field | Type |
| ------------- | -------- |
| `title` | `string` |
| `subtitle` | `string` |
| `description` | `string` |
| `artist` | `string` |
| `imageUri` | `string` |
### `NitroSourceRetentionConfig`
| Field | Type | Purpose |
| ------------------ | ------------------------------------ | ----------------------------------- |
| `preload` | `'none' \| 'metadata' \| 'buffered'` | Initial preload depth |
| `offscreen` | `'cold' \| 'metadata' \| 'hot'` | Offscreen retention level |
| `trimDelayMs` | `number` | Delayed trim window |
| `feedPoolEligible` | `boolean` | Participate in native feed hot pool |
### `NitroSourceTransportConfig`
| Field | Type | Purpose |
| ------ | ------------------------------- | ---------------------------------- |
| `mode` | `'auto' \| 'direct' \| 'proxy'` | Transport routing (default `auto`) |
`auto` prefers the shared HLS proxy and falls back to direct when the proxy cannot be readied for the active source generation. `direct` skips proxy routing. `proxy` forces proxy routing.
### `NitroSourcePreviewConfig`
| Field | Type | Default | Purpose |
| --------------- | ------------------------------------ | ---------- | ---------------------------------------------------- |
| `mode` | `'listener' \| 'always' \| 'manual'` | `listener` | Automatic first-frame behavior |
| `autoThumbnail` | `boolean` | `true` | Native auto-thumbnail placeholder for attached views |
| `maxWidth` | `number` | `480` | Output width hint |
| `maxHeight` | `number` | `480` | Output height hint |
| `quality` | `number` | `70` | JPEG quality hint |
See [docs/source-config.md](docs/source-config.md) for source profiles and identity rules, and [docs/buffer-config.md](docs/buffer-config.md) for `BufferConfig` (Android `minBufferMs`, `maxBufferMs`, iOS `preferredForwardBufferDurationMs`, `preferredPeakBitRate`, shared `livePlayback`).
## `NitroPlayerView`
| Prop | Type | Notes |
| --------------------- | ------------------------------- | ------------------------------------- |
| `source` | `NitroSourceConfig` | Required |
| `playerDefaults` | `NitroPlayerDefaults` | Declarative startup state |
| `controls` | `boolean` | Native controls |
| `resizeMode` | `ResizeMode` | `contain \| cover \| stretch \| none` |
| `keepScreenAwake` | `boolean` | Screen wake lock |
| `surfaceType` | `'surface' \| 'texture'` | Android only |
| `style` | `ViewStyle` | Inherited from `ViewProps` |
| `onAttached` | `(player: NitroPlayer) => void` | View attached |
| `onDetached` | `() => void` | View detached |
| `onFullscreenChange` | `(value: boolean) => void` | Fullscreen state changed |
| `willEnterFullscreen` | `() => void` | Pre-enter hook |
| `willExitFullscreen` | `() => void` | Pre-exit hook |
### `NitroPlayerViewRef`
| Member | Type | Notes |
| ----------------------------------- | ---------------------- | ---------------------------------- |
| `player` | `NitroPlayer` | Imperative player backing the view |
| `isAttached` | `boolean` | Native attach state |
| `enterFullscreen()` | `void` | Enter fullscreen |
| `exitFullscreen()` | `void` | Exit fullscreen |
| `addEventListener(event, listener)` | `ListenerSubscription` | View events only |
## `NitroPlayer`
### Properties
| Property | Type | Get | Set |
| ------------------------ | ------------------------ | --- | --- |
| `source` | `NitroPlayerSource` | yes | no |
| `status` | `NitroPlayerStatus` | yes | no |
| `playbackState` | `PlaybackState` | yes | no |
| `memorySnapshot` | `MemorySnapshot` | yes | no |
| `duration` | `number` | yes | no |
| `currentTime` | `number` | yes | yes |
| `volume` | `number` | yes | yes |
| `muted` | `boolean` | yes | yes |
| `loop` | `boolean` | yes | yes |
| `rate` | `number` | yes | yes |
| `isPlaying` | `boolean` | yes | no |
| `isBuffering` | `boolean` | yes | no |
| `isVisualReady` | `boolean` | yes | no |
| `bufferDuration` | `number` | yes | no |
| `bufferedPosition` | `number` | yes | no |
| `mixAudioMode` | `MixAudioMode` | yes | yes |
| `ignoreSilentSwitchMode` | `IgnoreSilentSwitchMode` | yes | yes |
| `playInBackground` | `boolean` | yes | yes |
| `playWhenInactive` | `boolean` | yes | yes |
### Methods
| Method | Returns | Notes |
| ----------------------------------- | ---------------------- | ------------------------------------- |
| `play()` | `void` | Start playback; valid before `onLoad` |
| `pause()` | `void` | Pause playback |
| `seekTo(seconds)` | `void` | Absolute seek |
| `seekBy(seconds)` | `void` | Relative seek |
| `initialize()` | `Promise` | Manual initialization |
| `preload()` | `Promise` | Warm source without starting playback |
| `replaceSourceAsync(source)` | `Promise` | Swap active source |
| `clearSourceAsync()` | `Promise` | Clear source and keep player reusable |
| `release()` | `void` | Terminal teardown |
| `addEventListener(event, listener)` | `ListenerSubscription` | Subscribe to a player event |
## `PlaybackState`
| Field | Type | Notes |
| ------------------- | ----------------------- | --------------------------------------------------------------------- |
| `status` | `NitroPlayerStatus` | `idle \| loading \| buffering \| playing \| paused \| ended \| error` |
| `currentTime` | `number` | Seconds |
| `duration` | `number` | Seconds |
| `bufferDuration` | `number` | Seconds buffered ahead |
| `bufferedPosition` | `number` | Absolute buffered position |
| `rate` | `number` | Effective playback rate |
| `isPlaying` | `boolean` | Native playing state |
| `isBuffering` | `boolean` | Native buffering state |
| `isVisualReady` | `boolean` | First visual frame readiness |
| `error` | `PlaybackError \| null` | Present when `status === 'error'` |
| `nativeTimestampMs` | `number` | Native event timestamp |
## Player Events
| Event | Payload |
| ------------------- | -------------------- |
| `onPlaybackState` | `PlaybackState` |
| `onLoadStart` | `onLoadStartData` |
| `onLoad` | `onLoadData` |
| `onError` | `PlaybackError` |
| `onFirstFrame` | `onFirstFrameData` |
| `onBandwidthUpdate` | `BandwidthData` |
| `onVolumeChange` | `onVolumeChangeData` |
Subscribe directly on the player instance:
```ts
const sub = player.addEventListener('onPlaybackState', state => {
console.log(state.status, state.currentTime);
});
// later
sub.remove();
```
## Stream Cache & Preview
| API | Returns | Purpose |
| ------------------------------------- | --------------------------------- | -------------------------------------- |
| `streamCache.prefetch(source)` | `Promise` | Warm transport and first segment cache |
| `streamCache.getStats()` | `Promise` | Total cache stats |
| `streamCache.getStats(source)` | `Promise` | Per-source stats, header-aware |
| `streamCache.clear()` | `Promise` | Clear stream disk cache |
| `videoPreview.getFirstFrame(source)` | `Promise` | Cached or generated first-frame path |
| `videoPreview.peekFirstFrame(source)` | `Promise` | Cached-only lookup (no generation) |
| `videoPreview.clear()` | `Promise` | Clear preview artifacts |
All source-taking methods accept either a URL string or `{ uri, headers }`. Use the object form whenever headers are part of request identity. See [docs/stream-runtime.md](docs/stream-runtime.md).
## Hooks
| Hook | Signature | Purpose |
| ------------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `usePlaybackState` | `(player: NitroPlayer \| null \| undefined) => PlaybackState \| null` | Subscribes to `onPlaybackState` and returns latest snapshot |
| `useEvent` | `(player, event, callback) => void` | Managed event subscription with automatic cleanup |
## `createNitroSource`
```ts
import { createNitroSource } from '@noma4i/nitro-play';
const source = createNitroSource({ uri: require('./intro.mp4') });
```
Resolves React Native asset references via `Image.resolveAssetSource`, validates the URI, and returns a hybrid `NitroPlayerSource`. Usually you do not need to call this manually: `NitroPlayerView` and `NitroPlayer` accept a plain `NitroSourceConfig` and normalize it internally. Call `createNitroSource` explicitly when you want to reuse the same native source object across multiple `replaceSourceAsync` calls.
## Common Types
| Type | Values / Shape | Purpose |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `NitroPlayerStatus` | `idle \| loading \| buffering \| playing \| paused \| ended \| error` | Player status |
| `ResizeMode` | `contain \| cover \| stretch \| none` | View scaling |
| `MixAudioMode` | `mixWithOthers \| doNotMix \| duckOthers \| auto` | Audio session mix behavior |
| `IgnoreSilentSwitchMode` | `auto \| ignore \| obey` | iOS silent switch handling |
| `PreloadLevel` | `none \| metadata \| buffered` | Initial preload depth |
| `OffscreenRetention` | `cold \| metadata \| hot` | Offscreen retention level |
| `MemoryRetentionState` | `cold \| metadata \| hot` | Retention snapshot value |
| `MemorySnapshot` | `{ playerBytes, sourceBytes, totalBytes, preloadLevel, retentionState, isAttachedToView, isPlaying }` | Current player memory footprint |
| `NitroPlayerDefaults` | `{ loop?, muted?, volume?, rate?, mixAudioMode?, ignoreSilentSwitchMode?, playInBackground?, playWhenInactive? }` | Declarative startup state for `NitroPlayerView.playerDefaults` |
| `NitroPlayerError` | Discriminated on `LibraryError \| PlayerError \| SourceError \| NitroPlayerViewError \| UnknownError` | Typed error hierarchy used by `onError` and thrown runtime errors |
## Runtime Contract
| Area | Behavior |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Early play | `play()` before `onLoad` is canonical |
| Playback state | Built from native readiness, buffering, and actual playing state |
| HLS startup | Native runtime uses lazy startup and bounded startup recovery |
| Proxy fallback | `transport.mode='auto'` may fall back to direct URL if proxy is unavailable |
| First frame | `onFirstFrame` is sticky per active source generation |
| Preview policy | `preview.mode='listener'` auto-captures for attached views when `autoThumbnail !== false`; `always` warms preview automatically; `manual` disables background auto-warmup |
| Mounted-view reveal | Attached `NitroPlayerView` owns native auto-thumbnail/first-frame placeholder by default; app code should not require JS poster swapping for active playback surfaces |
| Manual preview | `videoPreview.getFirstFrame(source)` returns cached/generated frame path |
| Cached preview reuse | `videoPreview.peekFirstFrame(source)` returns only an existing cached frame path and never starts generation |
| Stream/preview identity | `{ uri, headers }` is the canonical identity for cache stats and preview artifacts |
| Stream cache | `streamCache.prefetch(source)` is safe to call repeatedly |
Absolute local file paths are accepted on both iOS and Android and are normalized internally to `file://` URLs. App code should prefer canonical `file://` URIs when it owns freshly recorded media paths.
## Example App
The local [example](example/README.md) is a runtime lab, not just a smoke test.
It covers:
- hero playback switching between `transport.mode='auto'`, header-isolated HLS, and direct MP4
- `streamCache.prefetch/getStats/clear` and `videoPreview.getFirstFrame/peekFirstFrame/clear`
- a three-player feed stress block with the same HLS URL under different headers
- live observation of `onLoad`, `onError`, `onFirstFrame`, bandwidth, attach state, and `isVisualReady`
## Documentation
| File | Purpose |
| ------------------------------------------------------ | ------------------------------------------------------- |
| [docs/player-api.md](docs/player-api.md) | Public player, view, events, hooks |
| [docs/source-config.md](docs/source-config.md) | Source DSL and normalized source model |
| [docs/buffer-config.md](docs/buffer-config.md) | `buffer` tuning |
| [docs/lifecycle-guide.md](docs/lifecycle-guide.md) | `retention` model and startup intent |
| [docs/memory-management.md](docs/memory-management.md) | Retention states, snapshots, feed pool |
| [docs/stream-runtime.md](docs/stream-runtime.md) | Shared transport runtime, `streamCache`, `videoPreview` |
| [docs/migration-1.0.md](docs/migration-1.0.md) | Migration to the current beta DSL |
## Requirements
| Dependency | Version |
| -------------------------- | ----------- |
| React Native | `>= 0.77.0` |
| react-native-nitro-modules | `>= 0.35.0` |
## License
MIT