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-stream-scroll

v0.1.0

Published

High-performance vertical live-stream pager for React Native — fused UICollectionView/ViewPager2 with an AVPlayer/ExoPlayer pool, native gestures, neighbor preload, and PiP. Built on Nitro Modules.

Readme

react-native-stream-scroll

High-performance vertical live-stream pager for React Native. Fuses the pager and the video pipeline into a single native view so swipes, snapping, and playback all run off the JS thread.

  • iOS: UICollectionView + AVPlayer pool (HLS + VOD)
  • Android: ViewPager2 + Media3 ExoPlayer pool
  • Native gestures (tap, long-press) — no JS-thread latency
  • Active player + N preloaded neighbors with LRU eviction
  • VOD position restore across recycles
  • Picture-in-Picture
  • Overlays stay as RN children (chat, buttons, sheets) — full JSX flexibility on top

Built on Nitro Modules.

Requirements

  • React Native 0.78.0+ (Nitro Views require the new architecture)
  • iOS 13+, Android API 24+
  • react-native-nitro-modules peer dep

Install

npm install react-native-stream-scroll react-native-nitro-modules
cd ios && pod install

Usage

import React, { useState } from 'react'
import { View, Text } from 'react-native'
import { StreamScrollView, StreamSource } from 'react-native-stream-scroll'

const STREAMS: StreamSource[] = [
  { id: 'a', url: 'https://.../stream-a.m3u8', isLive: true },
  { id: 'b', url: 'https://.../stream-b.m3u8', isLive: true },
  { id: 'c', url: 'https://.../past-stream.mp4', isLive: false, initialPositionMs: 0 },
]

export default function Live() {
  const [index, setIndex] = useState(0)

  return (
    <View style={{ flex: 1 }}>
      <StreamScrollView
        style={{ flex: 1 }}
        streams={STREAMS}
        activeIndex={index}
        isMuted={false}
        isPaused={false}
        preloadCount={1}
        loop
        resizeMode="cover"
        pipEnabled
        endReachedThreshold={2}
        onIndexChange={({ index }) => setIndex(index)}
        onEndReached={() => {/* fetch next page */}}
        onTap={({ index, x, y }) => {/* toggle controls */}}
      />
      {/* Overlays are plain RN children rendered above the native surface */}
      <ChatOverlay />
      <ActionButtons />
    </View>
  )
}

API

<StreamScrollView />

Props

| Prop | Type | Notes | |---|---|---| | streams | StreamSource[] | The feed. Identified by id for recycling + position cache. | | activeIndex | number | Current page. Both source-of-truth and sink (echoes back on swipe). | | isMuted | boolean | Applies to all pooled players. | | isPaused | boolean | Pauses the active player. | | preloadCount | number | Neighbors to keep buffered each side. Default 1. Pool capacity = 2 * preloadCount + 2. | | loop | boolean | Loop the active stream when it ends. | | resizeMode | 'cover' \| 'contain' \| 'stretch' | Video gravity. | | showNativeControls | boolean | iOS placeholder; Android uses ExoPlayer controls. Default false — overlays handle controls. | | pipEnabled | boolean | Allow Picture-in-Picture. | | endReachedThreshold | number | Fire onEndReached when within this many pages of the end. Default 2. |

Events

| Event | Payload | |---|---| | onIndexChange | { index } — fires after user swipe settles, not on JS-driven setActiveIndex. | | onPlaybackStateChange | { index, state }'idle' \| 'loading' \| 'ready' \| 'playing' \| 'paused' \| 'ended' \| 'error'. | | onProgress | { index, positionMs, durationMs, bufferedMs } — 4Hz. | | onError | { index, code, message } | | onTap | { index, x, y } | | onLongPress | { index, state: 'began' \| 'ended' } | | onScrollStateChange | { state: 'idle' \| 'dragging' \| 'settling' } | | onLoadStart | { index } | | onReadyForDisplay | { index } | | onEndReached | () — fires once per streams array identity, until streams change. |

Imperative methods (via hybridRef)

import { callback } from 'react-native-nitro-modules'

<StreamScrollView
  hybridRef={callback((ref) => {
    ref.play()
    ref.pause()
    ref.seek(15_000)
    ref.scrollToIndex(3, true)
    ref.refresh()                   // re-fetch active stream
    ref.clearCachedPosition('xyz')  // forget VOD seek position
    ref.enterPictureInPicture()
    ref.exitPictureInPicture()
  })}
/>

StreamSource

type StreamSource = {
  id: string                     // unique key — drives recycling + position cache
  url: string                    // HLS m3u8 or progressive MP4
  isLive: boolean                // changes buffer + retry policy
  posterUrl?: string             // shown until first frame
  headers?: Record<string, string>  // e.g. Cloud-CDN-Cookie
  initialPositionMs?: number     // VOD seek-on-load
}

Performance notes

  • One active player. Only the foreground page calls play(); neighbors are buffered but paused.
  • Pool eviction. When streams changes, players for removed ids are released. LRU caps to 2 * preloadCount + 2.
  • Position cache survives eviction. VOD streams scrolled away from and back to resume at the same point.
  • Gestures off-thread. Tap / long-press fire from UITapGestureRecognizer / Android GestureDetector — no JS-bridge latency.
  • Single AVAudioSession setup. Configured once per process at view init.

Integrating with zoop-buyer-app

To replace the existing StreamList.tsx + UnifiedVideoPlayer.tsx pair:

  1. Map LiveStreamV2 -> StreamSource (use the stream id as StreamSource.id, the signed HLS URL as url, the existing Cloud-CDN-Cookie cookie as a header entry).
  2. Move the existing Redux activeIndex selector to drive activeIndex prop; listen to onIndexChange to commit user swipes back.
  3. Keep LiveStreamOverlay, StreamChat, ActionButtons etc. as children of the <StreamScrollView> — they keep all their props/dispatch wiring.
  4. The existing globalPlaybackPositionCache Map becomes redundant — the native pool restores VOD position on its own.

Build

npm install
npm run codegen   # nitrogen -> generates Swift/Kotlin specs + JSON config
npm run build

After every change to src/specs/*.nitro.ts re-run npm run codegen so the native spec base classes pick up the new prop/method surface.

Status

v0.1 — core pager + video pipeline + gestures + PiP implemented. Not yet published. Wired through Nitro Modules code generation.