Skip to content

reechy-tools/recorder

Repository files navigation

Reechy

@reechy-tools/recorder

The open-source browser recording library behind Reechy.
Screen + camera PIP · draggable overlay · shape masks · audio mixing · zero server · TypeScript.

npm bundle size downloads MIT TypeScript


// 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

About

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

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors