npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-nitro-vlc

v1.0.0

Published

A libVLC 3.7 video player for React Native, built on Nitro Modules and tuned for live streaming on tvOS and Android TV.

Readme

react-native-nitro-vlc

A libVLC 3.7 video player for React Native that does not block your JS thread. Built on Nitro Modules, tuned for live streaming on tvOS and Android TV.

npm license React Native platforms

Most React Native video wrappers are fine until the stream dies. Then stop() blocks on the demuxer, the bridge call sits on the JS thread, and the whole app freezes for four seconds. This library is built so that cannot happen: every command is queued onto a per-player background thread and returns immediately, and every state read hits a native cache instead of libVLC.


Five claims, and how to falsify them

Every guarantee below is checkable in the bundled example app, which doubles as the benchmark harness. If one of these stops being true, the harness is where you find out.

| Guarantee | Why it holds | Check it yourself | |---|---|---| | The JS thread never blocks | Commands enqueue onto a per-player serial thread and return void. No Promises on hot paths. | Streams → Unreachable host, then press Stop. The UI stays live while libVLC unwinds. | | State reads are free | state, timeMs, videoWidth and friends read a native-side cache that libVLC events keep current. No getter ever calls into libVLC. | The stats overlay polls at 1 Hz and costs nothing measurable. | | Zapping is one hop | open(sources, config) does setMedia + failover list + play() in a single JS→native call. | Zap benchmarkopen()onFirstFrame, N channels × 3 rounds, min / avg / max. | | The player outlives the view | Player lifetime is fully independent of any view. Moving it re-attaches a surface. | Handoff — expand preview to fullscreen; the first-frame counter stays at 1 and position keeps running. | | Reconnect and failover are genuinely switchable off | With reconnect.enabled: false the library also stops passing :http-reconnect to libVLC. No hidden retries anywhere in the stack. | Streams → Failover demo — two dead URLs, then a live one. Watch onSourceChanged fire. |

Is this the right library for you?

| | | |---|---| | Good fit | Live TV and channel-zapping apps · raw MPEG-TS without a manifest · udp://@ / rtp://@ multicast on a LAN · RTSP · set-top boxes where stop() on a dead stream is a real freeze · anything needing libVLC's demuxer coverage | | Not a fit | DRM streams. libVLC has no Widevine, PlayReady or FairPlay support — use react-native-video instead. Also: teletext, and apps where a 60-80 MB per-ABI size increase is unacceptable. |


Requirements

| | Minimum | |---|---| | React Native | 0.78+ with the New Architecture (Nitro Views require it) | | iOS / tvOS | 15.1 | | Android | minSdk 23, compileSdk 36 | | tvOS testing | react-native-tvos |

Install

npm install react-native-nitro-vlc react-native-nitro-modules
cd ios && pod install

The podspec pulls MobileVLCKit ~> 3.7 on iOS and TVVLCKit ~> 3.7 on tvOS; Gradle pulls org.videolan.android:libvlc-all:3.7.0. Expect the app to grow by roughly 60-80 MB per ABI — libVLC bundles its own demuxers and decoders.

Android

Nothing to do. The library's manifest already declares INTERNET, CHANGE_WIFI_MULTICAST_STATE, the foreground-service permissions and the playback service; manifest merging pulls them into your app.

If you ship to Android TV, add this to your manifest so the app appears on the leanback launcher:

<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />

iOS / tvOS

Background audio (BackgroundBehavior 'audio-only' or 'continue') needs the audio background mode in your Info.plist — the library cannot add it for you:

<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
</array>

Quick start

import {
  useVlcPlayer,
  useVlcPlayerEvents,
  VlcVideoView,
} from 'react-native-nitro-vlc'

function Channel() {
  const player = useVlcPlayer()

  useVlcPlayerEvents(player, {
    onFirstFrame: () => console.log('picture!'),
    onError: (code, message) => console.warn(code, message),
  })

  React.useEffect(() => {
    player?.open([{ url: 'https://example.com/live.m3u8' }], {
      autoplay: true,
      latencyProfile: 'low',
    })
  }, [player])

  return <VlcVideoView player={player} scaleMode="fit" style={{ flex: 1 }} />
}

Why player?.useVlcPlayer() returns null for the first render and the player from then on. The player is allocated in an effect rather than during render because React may discard a render it has already started (a concurrent update interrupts it, an error boundary retries it, StrictMode runs it twice), and a player allocated by a render that never commits would never be unmounted, so nothing would dispose it. Need one outside a component? Use createVlcPlayer() and dispose it yourself.


Architecture

Three pieces, deliberately separate:

| | Role | |---|---| | VlcModule | The engine. One LibVLC instance per app, created lazily and reused by every player. Owns init options, device capabilities, AFR, Now Playing metadata and renderer discovery. | | VlcPlayer | A Hybrid Object wrapping one libVLC MediaPlayer. Created imperatively from JS. Every command and every event lives here. Its lifetime is completely independent of any view. | | VlcVideoView | A nearly dumb rendering surface. Two props: player and scaleMode. When player is set, native attaches the surface (attachViews on a VLCVideoLayout on Android, drawable on Apple). |

That split is what makes the two things live-TV apps care about cheap: channel zapping never recreates a surface, and moving playback between screens never restarts the stream.

Threading model

  • Every JS-facing command method enqueues and returns void. No Promises on hot paths, no thread hops you can observe.
  • Each player owns a serial queue: a HandlerThread on Android, a DispatchQueue on Apple.
  • libVLC events arrive on libVLC's own threads. Nitro callbacks are async-by-default, so they are invoked directly from there.
  • onTimeChanged is coalesced natively — 500 ms by default, tunable with setTimeUpdateInterval(ms). Raw libVLC TimeChanged events are never forwarded one-for-one.
  • Surface attach/detach runs on the platform UI thread.

Surface handoff

Detach ≠ stop. A view leaving the window only releases the surface. Playback is untouched.

During navigation two views briefly coexist, so the player tracks which view currently owns the surface. A detaching view that no longer owns it does nothing — otherwise the old screen's unmount would kill the new screen's video. On a player prop change the new surface is claimed first and the old one released after, keeping the black-frame window as short as the platform allows.

Android uses SurfaceView only (via VLCVideoLayout, which also provides the separate subtitle surface). Never TextureView: it adds a GPU composition pass and drops frames at 4K on cheap Amlogic boxes.

[!WARNING] Animating the video view's size jitters on low-end TV boxes. The surface is composited by the display pipeline, not the view system, so it lags the animated bounds by a frame or two. Snap to the final size when the animation ends instead of driving it frame by frame.


API

Engine

import {
  initializeVlc,
  getDeviceCapabilities,
  setAutoFrameRateMatching,
  setNowPlaying,
  enterPictureInPicture,
  startRendererDiscovery,
  stopRendererDiscovery,
  enableDebugLogging,
} from 'react-native-nitro-vlc'

initializeVlc(config?) is idempotent and optional — the first createVlcPlayer() boots the engine with defaults. Call it once, early, when you need any of:

initializeVlc({
  extraInitOptions: ['--avcodec-skiploopfilter=4'], // raw libVLC args
  timeshiftEnabled: true,
  timeshiftDirectory: '/data/.../timeshift',
  timeshiftMaxBytes: 256 * 1024 * 1024,
  audioPassthrough: true,   // Android: AC3/E-AC3/DTS over HDMI
  verboseLogging: __DEV__,
  maxConcurrentPlayers: 2,  // mosaic guard, default 2
})

Engine settings cannot change after the engine exists.

Player

const player = createVlcPlayer()          // or useVlcPlayer()

Media & zapping

| Method | Notes | |---|---| | open(sources, config) | The zap call. sources doubles as the failover list. | | play() / pause() / stop() | All non-blocking. | | seekTo(ms) / seekBy(deltaMs) | | | setRate(rate) | | | setVolume(0..200) | Above 100 is software amplification. | | setMuted(muted) | |

Cached state (synchronous, never touches libVLC): state, timeMs, durationMs, isSeekable, isPlaying, videoWidth, videoHeight, currentSourceIndex, isRecording.

Tracks: getAudioTracks(), getSubtitleTracks(), setAudioTrack(id), setSubtitleTrack(id) (-1 disables), addExternalSubtitle(url, select), setAudioDelay(ms), setSubtitleDelay(ms).

Track lists are cached natively and refreshed on elementary-stream changes. Listen for onTracksChanged and refetch.

Video: setDeinterlace(mode), setVideoAdjust(enabled, brightness, contrast, saturation, hue).

Audio: setStereoMode(mode), setEqualizerEnabled(on), setEqualizerPreset(index), setEqualizerBands(preampDb, bands). Preset names and band frequencies are exported as EQUALIZER_PRESETS and EQUALIZER_BAND_FREQUENCIES.

Recording: startRecording(directory), stopRecording(). The final path arrives via onRecordingChanged.

Casting: castTo(rendererId), stopCasting(). iOS and Android only — see the platform table.

Diagnostics: getStats(), getMediaInfo(), takeSnapshot(): Promise<ArrayBuffer>PNG bytes, zero-copy.

Behaviour: setBackgroundBehavior('pause' | 'audio-only' | 'continue'), default 'pause'.

disposeVlcPlayer(player) releases the media player, its queue and all listeners. Idempotent, and useVlcPlayer calls it for you.

Rendering a player through a <VlcVideoView> puts the Hybrid Object into a host component's props, and in __DEV__ React Fabric runs RN's deepFreezeAndThrowOnMutationInDev() over every prop it recognises — which freezes the player object itself. Nitro's dispose() runs the native teardown first and only then clears the object's NativeState; that last write is a defineProperty on a frozen object, so Hermes rejects it with "failed to define internal native state property". disposeVlcPlayer() swallows exactly that one error — the native player is already released when it fires — and rethrows anything else.

Events

player.setEventListeners({
  onStateChanged: state => {},
  onTimeChanged: ms => {},          // throttled natively
  onBuffering: percent => {},
  onFirstFrame: () => {},           // zap-time measurement point
  onVideoSizeChanged: (w, h) => {},
  onEndReached: () => {},
  onError: (code, message) => {},
  onReconnecting: (attempt, sourceIndex) => {},
  onReconnected: () => {},
  onSourceChanged: sourceIndex => {},
  onTracksChanged: () => {},
  onRecordingChanged: (isRecording, filePath) => {},
  onMetadataChanged: nowPlaying => {},
})

Or, in React, useVlcPlayerEvents(player, events) — it registers once, reads your handlers through a ref, and cleans up on unmount, so inline arrow functions cost nothing.

Keep-awake and remote controls

Both are automatic; there is no API to call.

While a player is playing with an attached view, Android sets FLAG_KEEP_SCREEN_ON on the surface and Apple sets isIdleTimerDisabled. Both are released on pause, stop, detach and dispose.

Transport controls are wired to a MediaSessionCompat (Android — TV remote keys and Assistant) and MPRemoteCommandCenter (Apple). setNowPlaying(title, artworkUrl?) feeds both. Android audio focus is handled app-wide: playback pauses on focus loss and ducks on a transient duckable loss.


Recipes

Zapping

Keep one player for the whole session and call open() per channel. The old media is torn down on the player's own thread while the JS thread carries on.

const zap = (channel: Channel) =>
  player.open(channel.sources, { autoplay: true, latencyProfile: 'low' })

latencyProfile presets (network caching): 'low' ≈ 800 ms (tuned for sports; also sets clock-jitter=0 / clock-synchro=0), 'normal' ≈ 1500 ms, 'safe' ≈ 3000 ms. networkCachingMs overrides the preset.

For an extra edge, ZapManager warms the next channel on a hidden second player:

const manager = new ZapManager()
manager.zapTo(channels[0].sources)
manager.prefetch(channels[1].sources)   // warms in the background
const active = manager.commit()          // swap; re-render the view with `active`

[!CAUTION] This costs two decoder sessions. Many cheap Android TV boxes have exactly one hardware video decoder, so the prefetching player falls back to software and can starve the visible one. Measure with the zap benchmark before shipping it.

List → fullscreen

Render the same player from two different views. Nothing else is needed:

{fullscreen
  ? <VlcVideoView player={player} style={StyleSheet.absoluteFill} />
  : <VlcVideoView player={player} style={styles.tile} />}

No reload, no position reset. On Android you may see a single black frame while the surface swaps; takeSnapshot() before the switch is the standard way to mask it.

Failover

Pass several URLs. When a source exhausts its retries the player advances to the next one and emits onSourceChanged:

player.open(
  [{ url: primary }, { url: backup }, { url: lastResort }],
  {
    autoplay: true,
    reconnect: { enabled: true, maxRetries: 5, initialDelayMs: 500, maxDelayMs: 8000 },
    stallWatchdog: { enabled: true, stallTimeoutMs: 8000 },
  }
)

Retries use exponential backoff. maxRetries: -1 retries forever. The stall watchdog independently restarts the current source when state === 'playing' but timeMs stops advancing.

Both are genuinely off when disabled. With reconnect.enabled: false the library also stops passing :http-reconnect to libVLC, so there are no hidden retries anywhere in the stack.

Auto frame rate (AFR)

setAutoFrameRateMatching(true)

On the first frame the display switches to a refresh rate that is an integer multiple of the stream's frame rate, and it is restored on stop/dispose. This is what removes 25 fps-on-60 Hz judder.

  • Android TV: picks the best Display.Mode at the current resolution and sets preferredDisplayModeId on the current Activity's window. Needs a foreground Activity; degrades to a logged no-op otherwise.
  • tvOS 17+: AVDisplayManager.preferredDisplayCriteria. Needs tvOS 17 because that is where AVDisplayCriteria(refreshRate:formatDescription:) became public API, and the viewer must have "Match Content → Match Frame Rate" enabled in tvOS settings.
  • iOS: no-op. The display already adapts and no equivalent API exists.

Multicast

udp://@239.0.0.1:1234 and rtp://@… work out of the box. On Android the library acquires a WifiManager.MulticastLock automatically for those schemes and releases it on stop/dispose — without it, Wi-Fi hardware filters the traffic and you get a black screen with no error.

Timeshift

initializeVlc({ timeshiftEnabled: true, timeshiftMaxBytes: 256 * 1024 * 1024 })

Once enabled, pausing a live stream buffers to disk and resume/seek-back works.

[!NOTE] Storage caveat. libVLC 3 has no total-size cap of its own. timeshiftMaxBytes sets the per-chunk granularity and is enforced by this library pruning the timeshift directory at engine startup — which is where space from a crashed session is reclaimed. Nothing is deleted while a stream is playing, because those files are live. On a box with 4 GB of storage, budget conservatively.

Background audio (radio)

player.setBackgroundBehavior('audio-only')

Android drops the video elementary stream and starts a media foreground service (FOREGROUND_SERVICE_MEDIA_PLAYBACK, already declared by the library). Apple sets the AVAudioSession category to .playback — your app still needs the audio background mode in its Info.plist.

'pause' (the default) pauses on background. 'continue' keeps decoding, which is what a PiP scenario wants.


Platform differences

These are real libVLC/binding limitations, not omissions.

| Feature | iOS / tvOS | Android | |---|---|---| | Buffering percentage | Coarse: 0 on entering, 100 on leaving. VLCKit 3 does not surface libVLC's buffering value. | Real percentages. | | Deinterlace at runtime | Live, via setDeinterlace:withFilter:. | Applied as a media option; changing it re-opens the current source at the current position (one re-buffer). VLC-Android does the same thing. | | Video adjust | Live, via adjustFilter. | Applied at open time. A change takes effect on the next open(). | | Stereo mode | Live, via audioChannel. | Media option; changing it re-opens the source. | | First frame detection | videoSize is polled once per frame during the open→first-frame window (VLCKit has no vout callback). Accurate to ~1 frame. | Driven by libVLC's Vout event. | | Snapshot | VLCKit writes a PNG to a temp file; it is read into an ArrayBuffer and deleted. | PixelCopy off the SurfaceView, encoded to PNG. Needs Android 7.0+ and an attached view. | | Picture-in-Picture | Always false. VLCKit 3 renders into a UIView, which AVPictureInPictureController cannot adopt. | Real PiP on API 26+ (returns whether it was requested; the transition completes asynchronously). | | Debug log listener | Includes libVLC's own log stream when verboseLogging: true. | Library diagnostics only — libVLC 3's Java bindings expose no libvlc_log_set. Raw libVLC output goes to logcat under the VLC tag. | | Track language | From tracksInformation. | From the media's elementary streams, matched by track id. | | Passthrough | The OS negotiates it with the connected device; the flag is advisory. | --aout=audiotrack + setAudioDigitalOutputEnabled. | | Device capabilities | VideoToolbox + UIScreen. maxDecodeWidth/Height report the panel's native resolution, since VideoToolbox publishes no hard ceiling. | MediaCodecList + Display.HdrCapabilities. | | Casting / renderer discovery | iOS only. TVVLCKit ships without VLCRendererDiscoverer, VLCRendererItem and -setRendererItem:, so on tvOS startRendererDiscovery reports an empty list and castTo raises onError. | Works via libVLC's RendererDiscoverer. | | Auto frame rate | tvOS 17+ only (AVDisplayManager), and the viewer must have "Match Content → Match Frame Rate" on. No-op on iOS. | preferredDisplayModeId on API 23+. |

Headers

libVLC 3's HTTP access module exposes exactly two header knobs: http-user-agent and http-referrer. MediaSource.userAgent and MediaSource.referer map onto them, and matching keys inside headers are picked up too. Any other header is dropped, with a warning in the log. If your provider requires custom auth headers, put a token in the URL or proxy the stream — there is no way to pass them through libVLC 3.

Hardware decode

Android uses setHWDecoderEnabled(true, false)enabled, not forced — so an unusual codec falls back to software instead of showing a green screen. On Apple, VideoToolbox is on by default and left alone.

Insecure TLS

allowInsecureTls: true auto-accepts libVLC's certificate questions for the duration of that player's playback. Otherwise the dialog is dismissed and you get onError('access', …). This is a process-wide libVLC policy, so if any open player allows it, certificate questions are accepted.

Error codes

libVLC 3 reports playback failure without a reason code, so PlayerErrorCode is inferred:

| Code | When | |---|---| | access | Failed while still opening — the stream was never reachable. | | network | Failed after having played — the connection dropped. | | died | Every source in the failover list is exhausted. | | timeout | Reserved for watchdog-originated failures. | | codec, unknown | Everything else. |


Example app

example/ is the demo and the benchmark harness. It is where the five claims at the top of this README are verified.

cd example
npm install
npm run android      # phone or Android TV
npm run ios

| Screen | What it proves | |---|---| | Streams | The stream matrix: HLS live, HLS VOD, raw HTTP MPEG-TS, udp/rtp multicast, an interlaced source, a radio/ICY stream, a failover demo and an unreachable host. Every transport, track, delay, deinterlace, scale-mode, recording and snapshot control is wired here. Edit example/src/streams.ts to point at your own endpoints. | | Zap benchmark | Time from open() to onFirstFrame, N channels × 3 rounds, reported as min / avg / max. This is the regression gate for anything touching the zap path. Reconnect and the watchdog are disabled during the run so a retry cannot disguise a slow first attempt. | | Handoff | The list → fullscreen surface handoff: the first-frame counter stays at 1 and the position keeps running across the switch. | | Device | Hardware decode support, HDR, refresh rates, AFR toggle. |

Every screen is D-pad friendly, with a visible focus ring on all controls.

tvOS

React Native 0.85 no longer supports multiple platform targets in one app Podfile, so the example ships as an iOS/Android project. For a tvOS build, scaffold a TV project and drop example/src into it:

npx @react-native-community/cli@latest init TvExample --template @react-native-tvos/template-tv
cd TvExample && npm install react-native-nitro-vlc react-native-nitro-modules
cp -r ../example/src ./src
cd ios && pod install

The library itself builds for the tvOS SDK unchanged — the Swift sources use #if os(tvOS) import TVVLCKit #else import MobileVLCKit #endif and the podspec declares both dependencies.


Contributing

npm install
npx nitrogen        # after any change to src/specs/*.nitro.ts
npm run typecheck
npm run lint

Generated code lands in nitrogen/generated and is committed, per Nitro convention.

Changing anything on the zap path? Run the Zap benchmark screen before and after, and put both numbers in the PR.

See AGENTS.md if you are working with an AI coding agent.


License

MIT for this library's own code — see LICENSE.

libVLC is LGPL 2.1. This library links it dynamically: MobileVLCKit/TVVLCKit ship as dynamic .xcframeworks and libvlc-all ships .so files. Do not repackage them into a static build unless you have read LGPL §6 and are prepared to satisfy it. Attribution and the LGPL text belong in your app's licenses screen.