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

lehar-render

v0.5.0

Published

A drop-in Remotion renderer: bundle an unmodified Remotion composition and render it to video with the lehar engine (headless-Chrome capture + in-process FFmpeg). renderMedia()-shaped API, a bundle/render CLI, and a live in-browser player with audible med

Readme

lehar-render

A drop-in Remotion renderer. Bundle an unmodified Remotion composition and render it to video with the lehar engine (headless-Chrome capture + in-process FFmpeg) instead of Remotion's own renderer — same compositions, a different (faster) engine underneath.

npm install lehar-render

Requires the lehar binary at render time: point LEHAR_BINARY_PATH at it, or have lehar on your PATH. (Automatic per-platform binary provisioning is planned; today the binary is supplied by the host — e.g. baked into a Docker image.)

Two steps: bundle, then render

1. Bundle the composition into a lehar template (a build-time step):

npx lehar-render bundle --entry src/remotion/index.ts --out dist/lehar-bundle --public public

2. Render it — programmatically, via the renderMedia-shaped API a service swaps in for @remotion/renderer:

import { renderMedia } from 'lehar-render';

await renderMedia({
  serveUrl: 'dist/lehar-bundle',   // the bundle dir from step 1
  outputLocation: 'out.mp4',
  durationInFrames: 150,
  fps: 30,
  width: 1920,
  height: 1080,
  inputProps: { /* your composition props */ },
  onProgress: ({ progress }) => console.log(`${Math.round(progress * 100)}%`),
});

…or from the CLI: npx lehar-render render --serve-url dist/lehar-bundle -o out.mp4 --duration-in-frames 150 --fps 30.

Audio: lehar captures pixels only, so <Audio> renders as a visual no-op — pass a mixed soundtrack via audioManifest (a lehar --audio-manifest JSON the caller computes from its timeline). See below.


The bundler esbuild-aliases remotion and @remotion/media-utils to this package; the composition's own entry point (registerRoot(...)) becomes a lehar-renderable index.html + bundle.js.

How it maps onto the lehar contract

  • registerRoot() mounts the tree and installs window.__LEHAR_READY. Per captured frame the hook: commits the new frame synchronously (flushSync), waits for every open delayRender handle (fonts, <Img> decodes, <Video> metadata, useAudioData decodes), then runs per-frame tasks (<Video> seek + canvas draw). It never awaits requestAnimationFrame.
  • Width/height/fps/duration come from window.__LEHAR (the CLI flags), not the <Composition> registration — the same override renderMedia() performs. Props merge {...defaultProps, ...window.__LEHAR_PROPS}.
  • <Video> implements the settled seek+draw design (docs/spike-video-sources.md): hidden <video>, visible <canvas>, drawImage inside the frame gate. Sources must be all-intra conformed.
  • <Audio> renders nothing: lehar captures pixels; the soundtrack is mixed in-process by the CLI from --audio-manifest (the invoking service computes the manifest from the same timeline data).
  • Opening the bundle in a plain browser (no lehar) shows frame 0 with the registered defaults — handy for debugging.

Supported surface (exactly what the host app's Timeline uses)

AbsoluteFill, Sequence (from, durationInFrames, layout="none"), Img, Video (startFrom, endAt, loop; volume/muted accepted and ignored), Audio (visual no-op), Composition, registerRoot, useCurrentFrame, useVideoConfig, interpolate, Easing, random, staticFile, delayRender/continueRender/cancelRender; @remotion/media-utils: useAudioData, visualizeAudio, getAudioData.

Anything else (spring, OffthreadVideo, <Series>, premountFor, playbackRate≠1, lazyComponent, ...) fails at bundle time with a "no matching export" error, or throws at mount — loud by design. Extend the shim deliberately, with a parity test, when a composition needs more.

Exactness

interpolate, Easing, random, staticFile and the whole visualizeAudio FFT chain are line-for-line ports of [email protected] (MIT), quirks included — tests/parity.test.mjs pins them bit-for-bit against vectors generated from the real packages (npm run gen-reference, which needs a node_modules containing remotion, e.g. the host app's). Regenerate the reference only when deliberately tracking a new upstream version.

Deliberate deviations (all invisible to the host app's composition):

  • getAudioData ids results with a counter, not Math.random() (lehar determinism dogma; it's only a cache key), and skips upstream's 3-concurrent-fetch throttle.
  • useAudioData commits its state via flushSync before clearing its delayRender handle. Under Remotion the renderer re-polls the page, so async scheduling is fine; under lehar the frame gate captures immediately after handles clear, so the decoded-state DOM must already be committed. Any future async-state-behind-delayRender in the shim must follow this rule.
  • <Img> fails the render on a broken image without upstream's retries.

Porting the host app Timeline

The whole port is one bundle invocation — the composition's own entry, unmodified:

node scripts/bundle.mjs --entry <remotion-project>/src/remotion/index.ts \
                        --out dist/timeline \
                        --public <remotion-project>/public
lehar render dist/timeline/index.html --props <props.json> --fps 25 --duration <s> ...

Validated bit-identical to real Remotion on photo/DOM frames and frame-exact on video frames — method, numbers, and the two operational requirements (asset URLs must be absolute; the asset server must support HTTP Range) in docs/timeline-port-validation.md. A reproduction props file is at examples/timeline-validation-props.json.

Live player (lehar-render/player)

The same contract that makes renders deterministic makes templates playable live: the player drives window.__LEHAR + await window.__LEHAR_READY from a requestAnimationFrame clock in an ordinary browser — no headless Chrome, no capture, no ffmpeg. Instant preview and free scrubbing; export stays lehar render. Design and measured numbers: docs/design-live-player.md in the lehar repo.

import { createLeharPlayer } from 'lehar-render/player';

const player = createLeharPlayer({
  container: document.querySelector('#preview'),
  src: '/templates/timeline/index.html', // same-origin, any lehar template
  fps: 25, width: 1920, height: 1080, durationInSeconds: 12,
  props: { clips: [] },                  // installed at document-start
});
player.play(); player.seek(120);
player.setProps(next); // live editing: re-presents the current frame

// React apps (host app provides React >= 18):
//   import { LeharPlayer } from 'lehar-render/player/react';
//   <LeharPlayer src="..." fps={25} width={1920} height={1080}
//                durationInSeconds={12} props={timelineProps} autoplay loop />

The player must be same-origin with the template (it reaches into the iframe's window). It loads the template through a document-start bootstrap that installs __LEHAR_PROPS / __LEHAR_COMPOSITION and sets window.remotion_staticBase to the template's directory, so staticFile() assets resolve when the template is served under a path prefix (under lehar render the template directory is the origin root, so this never comes up).

Controls chrome: the built-in bar auto-hides after ~2.5 s idle (playing or paused) so bottom-positioned template overlays (lyrics, captions) read exactly like the chrome-less capture; pointer activity, transport changes, or keyboard focus summon it back, and the cursor hides with it. The video area is one pointer surface owned by the player (a transparent overlay above the template iframe — required, or events over the video would vanish into the iframe and hidden chrome could never be summoned in a snug embed): click toggles play/pause, double-click toggles fullscreen, and on the focused player Space toggles, ArrowLeft/Right steps ±1 frame (Shift ±10), F fullscreens, M mutes. With controls: false no click behavior is attached, but pointer events still land on the host — bubbling to your own listeners — instead of disappearing into the iframe. Compositions are pure functions of __LEHAR.frame (non-interactive by contract), so the inert stage costs nothing.

Media playback (P2): the bootstrap also marks the document with window.__LEHAR_PLAYBACK, and the player injects per-frame transport into __LEHAR (playback/playing/playbackRate/volume — additive fields the renderer never sets, so capture output is untouched). Under a player, <Video> plays natively (drift-corrected against the player clock instead of capture's per-frame seek+draw) and <Audio> is a real audible element — an unmodified composition's music/clip audio, trims, loops, ducking and fades preview correctly because the audio markup is the same source of truth the render manifest is derived from. Requirements: the origin serving media must support HTTP Range (Chromium cannot seek unbuffered media without it — same requirement as the renderer's asset server), and audible autoplay is subject to browser policy: a blocked unmuted play() falls back to muted playback, the player emits 'autoplayblocked', and the controls' unmute button (a user gesture) restores sound. Set window.__LEHAR_MEDIA_TRACE = [] on the template window to log every media reconcile decision when debugging.

Pass prefetchMedia (player option / React prop) with the playable-media universe in playback order — every clip source plus the music bed, which the integrating app knows from its own timeline model. The shim treats the list as a schedule, not an order to buffer everything now: audio beds and a rolling window of the next few clips warm ahead of the playhead (bounded concurrency, re-anchored by every clip mount so it follows seeks both directions); everything beyond the window is untouched until the playhead approaches. Warm boundary mounts start without the cold-load silence the render wouldn't have, while a many-clip montage against a remote/metered media origin (presigned URLs, per-host connection limits) never sees the whole timeline requested at once. Playback-mode deviations: <Video> forwards style/className to the pooled media element with React semantics; other DOM props (id, crossOrigin, data attributes) are not forwarded, and a display:contents wrapper div is visible to structural selectors.

Tests

  • npm test — bit-for-bit parity of the pure ports (node:test, no build).
  • npm run build-smoke — regenerates tests/fixtures/remotion_shim_smoke/ in the lehar repo: a committed, built smoke composition whose probe squares (frame bit-strip, Sequence windowing, Img gating, props flow, WebAudio decode) are pixel-asserted by tests/shim_smoke.rs under the real engine, plus a bit-determinism check. Rebuild the fixture whenever shim runtime code changes, in the same commit.
  • node scripts/build-bench-timeline.mjs — regenerates lehar's FROZEN bench/timeline/ scenario (the host app Timeline + self-contained synthetic assets). Unlike the smoke fixture this is NOT rebuilt on shim changes: it is a benchmark workload, and regenerating it starts a new measurement series in bench/history.jsonl (pins in tests/constraints.rs; E2E guard in tests/bench_timeline.rs).