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

@busyexplore/zotonic-video-engine

v0.0.1-beta.0

Published

A React hook that wraps a canvas-based video compositor — feed it frames drawn to a `<canvas>`, an audio track, and a frame range, and it composites, crops, and exports a video. Processing/render state is broadcast as events via the re-exported `useWatchC

Readme

@busyexplore/zotonic-video-engine

A React hook that wraps a canvas-based video compositor — feed it frames drawn to a <canvas>, an audio track, and a frame range, and it composites, crops, and exports a video. Processing/render state is broadcast as events via the re-exported useWatchCommandEmitter, so distant components can react without prop drilling.

Features

  • 🖼️ Frame-by-frame canvas compositing — draw each frame yourself, hand it to the engine, and it assembles the video
  • ✂️ Center-crop support — render to a larger "working area" (e.g. for overscan/bleed) and crop down to the camera region on export
  • 🔊 Audio muxing — attach an audio source and a frame range to sync it against your rendered frames
  • 📡 Broadcasts processing/render lifecycle events (start, stop, rendering progress, update, error, ready) so a progress bar or toast can live anywhere in your app
  • ⚛️ Single hook API — useZotonicVideoEngine()

Installation

npm install @busyexplore/zotonic-video-engine
yarn add @busyexplore/zotonic-video-engine
pnpm add @busyexplore/zotonic-video-engine

Quick start

import { useRef, useState } from "react";
import {useZotonicVideoEngine} from "@busyexplore/zotonic-video-engine";

function Renderer() {
  const engine = useZotonicVideoEngine();
  const canvasRef = useRef(null);
  const [rendering, setRendering] = useState(false);

  const render = async () => {
    setRendering(true);

    engine.setExternalCanvasInfo(1280, 720, 2560, 1440); // camera size, working-area size
    engine.setFPS(24);
    engine.setTotalFrames(24 * 3); // 3 seconds

    await engine.loadAudio(audioInput, 0, 24 * 3);

    canvasRef.current.width = 2560;
    canvasRef.current.height = 1440;

    for (let frame = 0; frame < engine.getTotalFrames(); frame++) {
      const ctx = canvasRef.current.getContext("2d");
      // ...draw your scene for this frame onto ctx...

      await engine.drawFrame(frame, canvasRef.current, true); // true = crop to camera region
    }

    await engine.exportVideo(false, true, "animation.webm");
    setRendering(false);
  };

  return (
    <div>
      <canvas ref={canvasRef} width={1280} height={720} />
      <button onClick={render} disabled={rendering}>
        {rendering ? "Rendering…" : "Render"}
      </button>
    </div>
  );
}

API

useZotonicVideoEngine()

React hook, no arguments. Fires ACTION_READY shortly after mount. Returns:

| Key | Type | Description | |---|---|---| | setExternalCanvasInfo | (cameraWidth?, cameraHeight?, workingAreaWidth?, workingAreaHeight?) => void | Sets the visible "camera" (output) size and the larger "working area" you draw onto. Defaults to 1280×720 for all four if omitted. | | setFPS | (fps?: number) => void | Sets the frame rate (default 24). | | setTotalFrames | (totalFrames?: number) => void | Sets how many frames the render will produce (default 24). | | getTotalFrames | () => number | Returns the total frame count previously set. | | loadAudio | (audioInput, startFrame?: number, endFrame?: number) => Promise<void> | Attaches an audio source, trimmed/aligned to [startFrame, endFrame]. | | drawFrame | (currentFrame: number, sourceCanvas: HTMLCanvasElement, cropCenter?: boolean) => Promise<void> | Takes the frame you drew on sourceCanvas (at working-area size) and commits it as currentFrame. If cropCenter is true, crops down to the centered camera region set via setExternalCanvasInfo. | | nextFrame | (currentFrame?: number) => void | Advances the engine's internal frame cursor. | | getCanvaContext | () => CanvasRenderingContext2D | Returns the engine's internal canvas 2D context. | | exportVideo | (returnBuffer?: boolean, autoDownload?: boolean, fileName?: string) => Promise<Buffer \| void> | Finalizes and exports the rendered video. If returnBuffer is true, resolves with the file's buffer; if autoDownload is true, triggers a browser download as fileName (default "file.webm"). | | dispose | () => void | Tears down the current engine instance and replaces it with a fresh one, carrying over the current fps and totalFrames. | | useWatchCommandEmitter | hook | Re-exported so other components can subscribe to video-engine events. See Listening for events. |

setExternalCanvasInfo, setFPS, and setTotalFrames configure the next render — call them before your drawFrame loop, not mid-loop.

VIDEO_ENGINE_TOOL

Command/action name constants, importable directly:

import { VIDEO_ENGINE_TOOL } from "@busyexplore/zotonic-video-engine";

export const VIDEO_ENGINE_TOOL = {
  ACTION_START_PROCESSING: "START_PROCESSING",
  ACTION_STOP_PROCESSING: "STOP_PROCESSING",
  ACTION_RENDERING: "ACTION_RENDERING",
  ACTION_UPDATE: "update",
  ACTION_READY: "ready",
  ACTION_ERROR: "ERROR",
  VIDEO_ENGINE_COMMAND: "VIDEO_ENGINE",
};

| Action | Emitted when | |---|---| | ACTION_READY | The hook has mounted and the engine is ready to use | | ACTION_START_PROCESSING | A render/export operation begins | | ACTION_RENDERING | A frame has been drawn/committed — event data includes current (frame index), useful for progress tracking | | ACTION_UPDATE | Engine state has changed and fresh data is available — event data is the relevant payload | | ACTION_STOP_PROCESSING | A render/export operation finishes (success path) | | ACTION_ERROR | An operation (e.g. loading audio, exporting) failed — event data has a message |

All events are emitted under VIDEO_ENGINE_TOOL.VIDEO_ENGINE_COMMAND, so filter on that first, then switch on action.

Listening for events

Because useWatchCommandEmitter is re-exported from this package, any component can subscribe to render progress without receiving props from the component that owns the render loop. It subscribes for the lifetime of the calling component and cleans up automatically on unmount:

import  { useZotonicVideoEngine,VIDEO_ENGINE_TOOL, useWatchCommandEmitter } from "@busyexplore/zotonic-video-engine";

function RenderProgress() {
  const { useWatchCommandEmitter } = useWatchCommandEmitter();

  useWatchCommandEmitter((command, action, data) => {
    if (command !== VIDEO_ENGINE_TOOL.VIDEO_ENGINE_COMMAND) return;

    switch (action) {
      case VIDEO_ENGINE_TOOL.ACTION_START_PROCESSING:
        console.log("Render started…");
        break;
      case VIDEO_ENGINE_TOOL.ACTION_RENDERING:
        console.log("Frame:", data.current);
        break;
      case VIDEO_ENGINE_TOOL.ACTION_STOP_PROCESSING:
        console.log("Render finished");
        break;
      case VIDEO_ENGINE_TOOL.ACTION_ERROR:
        console.error(data.message);
        break;
    }
  });

  return null;
}

This is useful for surfacing a global progress bar or toast from a component that isn't the one calling drawFrame/exportVideo.

Behavior notes

  • drawFrame expects working-area coordinates — draw your full scene (including any off-camera bleed) at the workingAreaWidth/workingAreaHeight size set in setExternalCanvasInfo, then let cropCenter: true handle cropping down to the camera region on export.
  • Configure before you loop — setExternalCanvasInfo, setFPS, and setTotalFrames should be called once before the drawFrame loop starts; changing them mid-render is not supported.
  • Events are global, not scoped to the calling component — every subscriber in your app receives every emitted command, so filter by command (and action) in your listener.
  • dispose preserves fps/totalFrames — calling it tears down and replaces the underlying engine instance, but the frame rate and total frame count you'd already set carry over to the new instance.
  • exportVideo can both return and download — pass returnBuffer: true if you need the raw file (e.g. to upload it) in addition to, or instead of, triggering a browser download.

Happy rendering! 🎬