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

ytdlp-react-native

v0.1.0

Published

React Native / Expo port of the ytdlp-typescript engine: YouTube, TikTok, Facebook, Pinterest, VK, Telegram and BiliBili extraction without Node.js builtins

Readme

ytdlp-react-native

License: Unlicense

React Native / Expo port of the ytdlp-typescript engine — a TypeScript rewrite of the yt-dlp extraction core with zero Node.js builtins.

Supported platforms:

  • YouTube (innertube API client chain, sig + nsig challenge solving via a pure-JS interpreter, HLS/DASH manifests, subtitles, PO-token passthrough)
  • TikTok (universal-data scrape, WAF challenge solver, short-link redirects)
  • Facebook (ScheduledServerJS relay data, tahoe fallback)
  • Pinterest (resource API, master/media HLS playlists)
  • VK (al_video payload, JS salt challenge), Telegram (embed frames), BiliBili (wbi signing)

Zero runtime dependencies. Works on Hermes / JSC / V8-based React Native and Expo.

Why a separate package

The Node version relies on node:vm, node:fs, node:crypto and node:child_process. None of these exist in React Native. This package replaces them with an injectable runtime layer:

| Node builtin | ytdlp-react-native replacement | | --- | --- | | node:vm (player-JS sandbox) | Pure-JS interpreter (src/jsinterp) ported from upstream jsinterp.py concepts | | node:fs | FileSystem interface + MemoryFileSystem; Expo adapter included | | node:crypto | CryptoRuntime interface; pure-JS MD5/SHA-256/AES-128 included, react-native-quick-crypto adapter optional | | node:child_process (ffmpeg) | Merger interface; merging is opt-in via your own implementation | | Buffer | Uint8Array + base64 polyfill |

Install

npm install ytdlp-react-native
# or
bun add ytdlp-react-native

Optional peer dependencies for native performance:

npx expo install expo-file-system        # persistent downloads
npm install react-native-quick-crypto    # fast MD5/SHA/AES

Quick start (Expo)

import { extract, setRuntime } from 'ytdlp-react-native'
import { createExpoFileSystem } from 'ytdlp-react-native/dist/adapters/expo-file-system'
import { createQuickCrypto } from 'ytdlp-react-native/dist/adapters/quick-crypto'

setRuntime({
  fs: createExpoFileSystem(),
  crypto: createQuickCrypto(),
})

const info = await extract('https://www.tiktok.com/@user/video/123456')
console.log(info.title, info.formats.length)
info.formats.forEach((f) => console.log(f.format_id, f.ext, f.height, f.url))

If you only need format URLs (playback via expo-av / react-native-video), no filesystem setup is required at all.

Example app (App.tsx)

A complete, minimal Expo app: paste a URL, extract formats, tap to download with a live progress bar.

// App.tsx
import React, { useState } from 'react'
import {
  ActivityIndicator,
  Linking,
  Pressable,
  ScrollView,
  StatusBar,
  StyleSheet,
  Text,
  TextInput,
  View,
} from 'react-native'

import {
  download,
  extract,
  setRuntime,
  type InfoDict,
  type ProgressStatus,
} from 'ytdlp-react-native'
import { createExpoFileSystem } from 'ytdlp-react-native/dist/adapters/expo-file-system'
import { createQuickCrypto } from 'ytdlp-react-native/dist/adapters/quick-crypto'

// Configure once, before any extract/download call.
setRuntime({
  fs: createExpoFileSystem(),
  crypto: createQuickCrypto(),
})

function fmtDuration(s?: number | null): string {
  if (!s) return '-'
  const m = Math.floor(s / 60)
  return `${m}:${String(Math.round(s % 60)).padStart(2, '0')}`
}

export default function App() {
  const [url, setUrl] = useState('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
  const [busy, setBusy] = useState(false)
  const [info, setInfo] = useState<InfoDict | null>(null)
  const [status, setStatus] = useState('')
  const [percent, setPercent] = useState<number | null>(null)

  async function handleExtract() {
    setBusy(true)
    setInfo(null)
    setStatus('')
    try {
      setInfo(await extract(url.trim()))
    } catch (e) {
      setStatus(`Extract failed: ${(e as Error).message}`)
    } finally {
      setBusy(false)
    }
  }

  async function handleDownload() {
    if (!info) return
    setBusy(true)
    setPercent(0)
    try {
      const files = await download(info, {
        format: 'b',
        outtmpl: '%(title)s [%(id)s].%(ext)s',
        onProgress: (p: ProgressStatus) => {
          if (p.downloaded_bytes != null && p.total_bytes) {
            setPercent(Math.round((p.downloaded_bytes / p.total_bytes) * 100))
          }
        },
      })
      setStatus(`Saved: ${files[0].filepath}`)
    } catch (e) {
      setStatus(`Download failed: ${(e as Error).message}`)
    } finally {
      setBusy(false)
      setPercent(null)
    }
  }

  return (
    <View style={styles.root}>
      <StatusBar barStyle="light-content" />
      <Text style={styles.title}>ytdlp</Text>

      <TextInput
        style={styles.input}
        value={url}
        onChangeText={setUrl}
        placeholder="Video URL"
        placeholderTextColor="#666"
        autoCapitalize="none"
      />

      <Pressable
        style={[styles.btn, busy && styles.btnDisabled]}
        onPress={handleExtract}
        disabled={busy}
      >
        {busy ? (
          <ActivityIndicator color="#fff" />
        ) : (
          <Text style={styles.btnLabel}>Extract</Text>
        )}
      </Pressable>

      {info && (
        <ScrollView style={styles.card} showsVerticalScrollIndicator={false}>
          <Text style={styles.videoTitle} numberOfLines={2}>
            {info.title}
          </Text>
          <Text style={styles.meta}>
            {info.uploader ?? info.channel ?? ''} · {fmtDuration(info.duration)} ·{' '}
            {info.formats.length} formats
          </Text>

          <Pressable
            style={[styles.btn, styles.dlBtn, busy && styles.btnDisabled]}
            onPress={handleDownload}
            disabled={busy}
          >
            <Text style={styles.btnLabel}>
              Download best{percent !== null ? ` — ${percent}%` : ''}
            </Text>
          </Pressable>

          {status !== '' && <Text style={styles.status}>{status}</Text>}

          <Text style={[styles.meta, styles.listHeader]}>Formats (tap URL opens):</Text>
          {info.formats.slice(0, 10).map((f) => (
            <Text
              key={String(f.format_id)}
              style={styles.formatRow}
              onPress={() => f.url && Linking.openURL(f.url)}
            >
              {String(f.format_id).padEnd(8)}
              {f.ext.padEnd(5)}
              {f.height ? `${f.height}p` : 'audio'}
              {f.tbr ? `  ${Math.round(f.tbr)}kbps` : ''}
            </Text>
          ))}
        </ScrollView>
      )}
    </View>
  )
}

const styles = StyleSheet.create({
  root: { flex: 1, backgroundColor: '#111', padding: 20, paddingTop: 70 },
  title: { color: '#7dd3fc', fontSize: 28, fontWeight: '800', marginBottom: 16 },
  input: {
    backgroundColor: '#1e1e1e',
    borderColor: '#333',
    borderWidth: 1,
    borderRadius: 8,
    color: '#eee',
    paddingHorizontal: 12,
    paddingVertical: 10,
    marginBottom: 12,
  },
  btn: {
    backgroundColor: '#2563eb',
    borderRadius: 8,
    paddingVertical: 12,
    alignItems: 'center',
  },
  dlBtn: { marginTop: 14, backgroundColor: '#059669' },
  btnDisabled: { opacity: 0.5 },
  btnLabel: { color: '#fff', fontWeight: '600' },
  card: { marginTop: 18 },
  videoTitle: { color: '#fff', fontSize: 17, fontWeight: '600' },
  meta: { color: '#999', fontSize: 13, marginTop: 6 },
  listHeader: { marginTop: 16, marginBottom: 4 },
  status: { color: '#fbbf24', fontSize: 13, marginTop: 10 },
  formatRow: { color: '#93c5fd', fontFamily: 'monospace', fontSize: 12, paddingVertical: 3 },
})

Notes:

  • format: 'b' picks the best already-muxed stream so no merge step is needed on-device.
  • The progress bar reflects bytes for HTTP downloads; HLS/DASH fragment downloads report fragment_index / fragment_count instead.

Downloading to a file

import { download, setRuntime } from 'ytdlp-react-native'

const files = await download('https://www.pinterest.com/pin/123456/', {
  format: 'b',
  outtmpl: '%(title)s [%(id)s].%(ext)s',
  onProgress: (p) => console.log(p.status, p.downloaded_bytes, p.total_bytes),
})

Notes on downloading on-device:

  • The HTTP downloader uses ranged chunk requests (default 8 MiB) so memory stays bounded even where RN fetch cannot stream response bodies.
  • Merged formats (bv*+ba) are skipped unless you provide a Merger. Without one, pass a muxed-friendly spec such as 'b' or 'bv*[acodec!=none]'.

Runtime injection

import { setRuntime } from 'ytdlp-react-native'
import type { YtDlpRuntime } from 'ytdlp-react-native'

const runtime: YtDlpRuntime = {
  fs: myFileSystem,       // required for download()
  crypto: myCrypto,       // required for TikTok challenges, VK salt, bilibili wbi, AES HLS
  js: myEvaluator,        // optional custom JS evaluator (defaults to built-in interpreter)
  merger: myMerger,       // optional ffmpeg-kit wrapper for bv*+ba merges
}
setRuntime(runtime)

The pure-JS fallbacks (MemoryFileSystem, PureJsCrypto, built-in interpreter) work everywhere but are slower: the TikTok WAF proof-of-work can take seconds to minutes without native SHA-256, and the interpreter is slower than node:vm for player challenges.

Options

| Option | Default | Description | | --- | --- | --- | | format | bv*+ba/b | yt-dlp-style selector (b, w, bv*, ba, filters like [height<=720], direct itags like 137+140) | | outtmpl | %(title)s [%(id)s].%(ext)s | Output filename template | | paths | – | { home } output directory (joined POSIX-style) | | cookieJar | – | Programmatic CookieJar (auth / private content) | | cookiesFromNetscape | – | Netscape cookies.txt as a string | | httpHeaders / userAgent | – | Extra headers | | retries / fragmentRetries | 10 | Network retry counts | | rateLimit | – | Bytes/sec throttle | | concurrentFragments | 1 | Parallel HLS/DASH fragment downloads | | poToken | – | { gvs?, player?, subs? } YouTube proof-of-origin tokens | | noMerge | false | Skip merged-format attempts | | onProgress | – | Progress callback (started / downloading / finished) |

Documentation

Extractors mirror upstream yt-dlp naming so cross-referencing the Python source stays easy. See the sibling project's docs for deep dives: docs/API.md, docs/format-selection.md.

Known limitations

  • Single videos only — playlists/channels not implemented
  • Live streams not supported
  • No ffmpeg merge out of the box (inject a Merger, e.g. around ffmpeg-kit-react-native)
  • The JS interpreter does not support regex literals, classes, generators or async/await (YouTube sig/nsig functions do not use them); if a future player change breaks nsig solving, pass a custom js evaluator backed by a WebView or react-native-nitro-modules
  • No browser impersonation equivalent; bot walls are mitigated per-platform via UA strategies

Development

bun install
bun run typecheck   # tsc --noEmit (strict; must stay clean)
bun run test        # vitest, offline unit tests
bun run build       # tsc -> dist/

Legal

This project does not host or distribute any copyrighted media; it is a technical client for platforms' publicly served streams. You are responsible for complying with each platform's Terms of Service and applicable copyright law in your jurisdiction. Not affiliated with YouTube, TikTok, Facebook/Meta, Pinterest, VK, Telegram or BiliBili.