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

@reechy-tools/recorder

v0.1.0

Published

OBS-style screen + camera recording in the browser. Draggable PIP, shape masks, audio mixing, zero server, TypeScript-first.

Readme


// Screen + camera PIP recording in 5 lines of code
const recorder = new BrowserRecorder({ screen: true, pip: { shape: 'rounded' } })
await recorder.init()
recorder.start()
// ... user records ...
const blob = await recorder.stop() // → WebM with composited screen + camera

Why this over RecordRTC?

RecordRTC hasn't shipped a release since 2021. No TypeScript, no PIP, no React hooks, 180KB.

| Feature | RecordRTC | @reechy-tools/recorder | |---------|:---------:|:----------------------:| | TypeScript — native, strict | ❌ | ✅ | | Screen + camera PIP | ❌ | ✅ Canvas compositing | | Draggable & resizable PIP | ❌ | ✅ Pointer events + rAF | | PIP shape masks (circle, rounded, square) | ❌ | ✅ | | Audio mixing without crashing MediaRecorder | ❌ | ✅ AudioContext graph | | Camera switch with freeze-frame (no flash) | ❌ | ✅ | | Add screen share mid-recording | ❌ | ✅ | | Max duration with auto-stop | ❌ | ✅ | | React hooks (useRecorder, usePip) | ❌ | ✅ | | Tailwind-ready data-* state attributes | ❌ | ✅ | | Structured errors with recovery hints | ❌ | ✅ | | AI-friendly (CLAUDE.md, llms.txt) | ❌ | ✅ | | Bundle size | ~180KB | ~10KB gzipped | | Last updated | 2021 | Active |


Install

npm install @reechy-tools/recorder
# pnpm add @reechy-tools/recorder
# yarn add @reechy-tools/recorder

React hooks are in a separate subpath — React is a peer dependency:

import { useRecorder, usePip } from '@reechy-tools/recorder/react'

Quick Start — Vanilla JS

import { BrowserRecorder } from '@reechy-tools/recorder'

const recorder = new BrowserRecorder({
  camera: { width: 1280, height: 720, facingMode: 'user' },
  screen: true,                            // enable screen share + PIP
  pip: { shape: 'rounded', mirrored: true },
  audio: true,
  countdown: 3,                            // 3-second countdown before recording
  codec: 'auto',                           // VP9 → VP8 → H264 → MP4
  maxDuration: 300,                        // auto-stop at 5 minutes
})

// 1. Request permissions (call on user gesture)
await recorder.init()

// 2. Bind to video elements
cameraVideoEl.srcObject = recorder.getCameraStream()

// 3. Listen to events
recorder.on('state-change',   (state) => console.log(state))
recorder.on('countdown',      (n)     => showCountdown(n))
recorder.on('duration',       (ms)    => updateTimer(ms))
recorder.on('duration-limit', ()      => console.log('Time is up — auto-stopped'))
recorder.on('error',          (err)   => console.error(err.code, err.recovery))

// 4. Start (with countdown)
recorder.start()

// 5. Mid-recording controls
recorder.toggleMic()                       // mute/unmute
recorder.toggleCamera()                    // hide/show camera
await recorder.toggleScreenShare()         // add/remove screen share
await recorder.switchCamera()              // front ↔ back (mobile)
recorder.setPipPosition({ x: 25, y: 75, width: 30 })
recorder.setPipShape('circle')

// 6. Stop → Blob
const blob = await recorder.stop()         // → Blob (WebM)
const url  = URL.createObjectURL(blob)

Quick Start — React

import { useRef } from 'react'
import { useRecorder, usePip } from '@reechy-tools/recorder/react'

export function RecordingPage() {
  const containerRef = useRef<HTMLDivElement>(null)

  const recorder = useRecorder({
    screen: true,
    pip: { shape: 'rounded', mirrored: true },
    maxDuration: 300,
  })

  const pip = usePip({
    containerRef,
    initialPosition: { x: 75, y: 75, width: 22 },
    onPositionChange: (pos) => recorder.setPipPosition(pos),
  })

  const handleStop = async () => {
    const blob = await recorder.stop()
    const url = URL.createObjectURL(blob)
    // upload, play back, or download
  }

  return (
    // Spread dataAttrs → Tailwind can target [data-recording], [data-paused], etc.
    <div
      ref={containerRef}
      {...recorder.dataAttrs}
      className="relative aspect-video w-full
        [&[data-recording]]:ring-2 [&[data-recording]]:ring-red-500
        [&[data-paused]]:opacity-60"
    >
      {/* Screen share background */}
      {recorder.screenStream && (
        <video
          ref={el => { if (el) el.srcObject = recorder.screenStream }}
          autoPlay muted playsInline
          className="h-full w-full object-contain"
        />
      )}

      {/* Draggable camera PIP */}
      {recorder.cameraStream && recorder.hasScreenShare && (
        <div {...pip.dragProps} style={{ ...pip.style, borderRadius: 12, overflow: 'hidden' }}>
          <video
            ref={el => { if (el) el.srcObject = recorder.cameraStream }}
            autoPlay muted playsInline
            className="h-full w-full object-cover [transform:scaleX(-1)]"
          />
          <div {...pip.resizeProps} />
        </div>
      )}

      {/* Camera only (no screen share) */}
      {recorder.cameraStream && !recorder.hasScreenShare && (
        <video
          ref={el => { if (el) el.srcObject = recorder.cameraStream }}
          autoPlay muted playsInline
          className="h-full w-full object-cover [transform:scaleX(-1)]"
        />
      )}

      {/* Countdown overlay */}
      {recorder.countdown !== null && (
        <div className="absolute inset-0 flex items-center justify-center">
          <span className="text-[120px] font-bold text-white">{recorder.countdown}</span>
        </div>
      )}

      {/* Controls */}
      <div className="absolute bottom-4 left-0 right-0 flex justify-center gap-2">
        {recorder.state === 'idle'  && <button onClick={recorder.init}>Allow Camera</button>}
        {recorder.state === 'ready' && <button onClick={recorder.start}>Record</button>}
        {recorder.isRecording && <>
          <button onClick={recorder.pause}>Pause</button>
          <button onClick={handleStop}>Stop</button>
          <button onClick={recorder.toggleScreenShare}>
            {recorder.hasScreenShare ? 'Stop Screen' : 'Share Screen'}
          </button>
        </>}
      </div>
    </div>
  )
}

API Reference

new BrowserRecorder(config?)

| Option | Type | Default | Description | |--------|------|---------|-------------| | camera | CameraConfig \| boolean | true | Camera constraints or false to disable | | screen | boolean | false | Enable screen capture (getDisplayMedia) | | pip.position | PipPosition | bottom-right | PIP position as percentages of container | | pip.shape | PipShape | 'rounded' | circle, rounded, square, or none | | pip.mirrored | boolean | true | Mirror camera horizontally | | audio | AudioConfig \| boolean | true | Audio constraints or false to disable | | countdown | number | 3 | Countdown seconds before recording (0 to skip) | | codec | CodecPreference | 'auto' | vp9, vp8, h264, av1, auto | | frameRate | number | 30 | Canvas recording frame rate | | maxDuration | number | undefined | Max recording seconds — auto-stops + emits duration-limit |

PipPosition

{ x: number, y: number, width: number }
// All values are percentages (0–100) relative to the container
// x, y = center point of the PIP
// width = PIP width (height derived from camera aspect ratio)

Events

recorder.on('state-change',    (state: RecorderState) => void)       // idle|ready|countdown|recording|paused|stopping
recorder.on('data',            (blob: Blob) => void)                 // raw MediaRecorder data chunks
recorder.on('error',           (err: RecorderError) => void)
recorder.on('countdown',       (seconds: number) => void)            // 3, 2, 1
recorder.on('duration',        (ms: number) => void)                 // fires every 100ms
recorder.on('duration-limit',  () => void)                           // maxDuration reached, auto-stop imminent
recorder.on('camera-change',   (enabled: boolean) => void)           // toggleCamera() result
recorder.on('screen-change',   (stream: MediaStream | null) => void) // screen share started/stopped
recorder.on('screen-ended',    () => void)                           // user stopped sharing via browser UI

useRecorder() return — React

All the above, plus:

recorder.dataAttrs  // spread on your container element
// {
//   'data-state': 'idle' | 'ready' | 'countdown' | 'recording' | 'paused' | 'stopping'
//   'data-recording'?: ''        // present only when recording
//   'data-paused'?: ''
//   'data-countdown'?: ''
//   'data-has-screen'?: ''
//   'data-error'?: ''
//   'data-duration-limit'?: ''
// }

Error codes

Every RecorderError has code, message, and recovery (user-facing hint).

| Code | When | |------|------| | CAMERA_PERMISSION_DENIED | User denied camera access | | CAMERA_NOT_FOUND | No camera device available | | CAMERA_IN_USE | Camera already in use by another app | | CAMERA_SWITCH_FAILED | Failed to switch front/back camera | | SCREEN_SHARE_DENIED | User cancelled screen picker | | SCREEN_SHARE_NOT_SUPPORTED | Mobile or old browser | | CODEC_NOT_SUPPORTED | No supported codec found | | RECORDER_NOT_INITIALIZED | init() not called before start() | | ALREADY_RECORDING | start() called while already recording | | NOT_RECORDING | stop()/pause() called when not recording | | AUDIO_CONTEXT_FAILED | AudioContext creation failed (iOS needs user gesture) | | CANVAS_CAPTURE_NOT_SUPPORTED | Safari — use a supported browser |


Browser Support

| Browser | Camera | Screen | PIP | |---------|:------:|:------:|:---:| | Chrome 88+ | ✅ | ✅ | ✅ | | Firefox 84+ | ✅ | ✅ | ✅ | | Edge 88+ | ✅ | ✅ | ✅ | | Safari 15+ | ✅ | ❌ | ❌ | | iOS Safari | ✅ | ❌ | ❌ | | Chrome Android | ✅ | ❌ | ❌ |


Examples

Run locally:

git clone https://github.com/reechy-tools/recorder
cd recorder && npm install
npm run dev   # builds + serves at http://localhost:4321
# open http://localhost:4321/examples/vanilla/

Made with @reechy-tools/recorder?

Add this badge to your project README:

[![Recorded with @reechy-tools/recorder](https://img.shields.io/badge/recorded_with-@reechy--tools%2Frecorder-0066FF?style=flat-square)](https://github.com/reechy-tools/recorder)

Used By

Built and battle-tested at Reechy — browser-based video pitch recorder for sales, hiring, and async communication. This library is extracted directly from Reechy's production codebase.


Contributing

PRs welcome. Please open an issue first for significant changes.

git clone https://github.com/reechy-tools/recorder
cd recorder && npm install
npm run dev     # tsup watch + local server at :4321
npm test        # vitest (24 tests)
npm run build   # production build → dist/

License

MIT © Reechy