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

visuality-embed

v0.2.1

Published

React embed SDK for **Visuality** — a prebuilt `<VideoRoom />`, headless hooks, and a **server-only** token helper. It wraps LiveKit's tested components (including their Safari/iOS workarounds) so you get a working call with a token and a server URL.

Readme

visuality-embed

React embed SDK for Visuality — a prebuilt <VideoRoom />, headless hooks, and a server-only token helper. It wraps LiveKit's tested components (including their Safari/iOS workarounds) so you get a working call with a token and a server URL.

The client contract (start here)

The <VideoRoom> component needs exactly two strings: { token, serverUrl }. That's the whole contract. Get them from your own authenticated endpoint — whatever proves who the caller is in your app — and hand them to the component:

"use client";
import { VideoRoom } from "visuality-embed";

export function Call({ token, serverUrl }: { token: string; serverUrl: string }) {
  return <VideoRoom serverUrl={serverUrl} token={token} onLeave={() => history.back()} />;
}

Minting that token is entirely up to you and is framework-agnostic: any runtime that can make an authenticated HTTP POST to the control plane's token endpoint works — Node, Deno, Bun, a Cloudflare Worker, or a Python/Go/Rails service that calls the same endpoint. This package ships a small createToken helper for the POST-from-Node case (see below), but it is a convenience, not a requirement — you can call the token endpoint however you like, from whatever server you already have.

Usually you don't hand-wire any of this: point your coding agent at the setup MCP and it scaffolds a <VideoRoom /> page + a Next.js token route into your app for you (see the top-level README). The Next.js route below is that scaffold's convenience implementation of "an authenticated endpoint that calls the token endpoint" — not the only valid one. This doc is the manual reference for what the SDK exposes.

Install

npm i visuality-embed
# peers you already have in a React app:
npm i react react-dom

The two halves: server mints the token, client joins

A publishable key can ship to the browser; the secret key must not. So token minting runs on your server, and the component runs in the browser with the resulting token.

1 · Server — mint a scoped token

createToken exchanges your project's secret key for a short-lived, room-scoped LiveKit token. It throws if called in a browser — keep it in a route handler / server action.

createToken lives in a separate server entry (visuality-embed/server) so importing it never pulls the client <VideoRoom> (and @livekit/components-react) into your server layer — that would throw createContext only works in Client Components and 500 the route.

// app/api/visuality-token/route.ts  (Next.js App Router)
import { createToken } from "visuality-embed/server";

export async function POST(req: Request) {
  const { room, identity } = await req.json();
  const { token, serverUrl } = await createToken({
    endpoint: process.env.VISUALITY_API_URL + "/token", // control-plane token endpoint
    publishableKey: process.env.VISUALITY_PUBLISHABLE_KEY!,
    secretKey: process.env.VISUALITY_SECRET_KEY!, // server-only — never NEXT_PUBLIC_*
    room,
    identity,
    template: "group", // a RoomTemplate from @vt/shared: "one_to_one" | "group" | "recording"
  });
  return Response.json({ token, serverUrl });
}

This is one implementation of "an authenticated endpoint" — Next.js isn't required. Any server that can call the control plane's token endpoint with your secret key and return { token, serverUrl } to the client satisfies the contract above.

2 · Client — drop in the room

"use client";
import { VideoRoom } from "visuality-embed";

export function Call({ token, serverUrl }: { token: string; serverUrl: string }) {
  return <VideoRoom serverUrl={serverUrl} token={token} onLeave={() => history.back()} />;
}

<VideoRoom> connects with audio+video and renders LiveKit's prebuilt VideoConference UI by default.

Controlling camera & mic on join

<VideoRoom> publishes both by default. To join with the camera off (no permission prompt, no frame published), pass video={false}:

<VideoRoom serverUrl={serverUrl} token={token} audio={micOn} video={cameraOn} />

Handling errors

<VideoRoom> accepts onError and onMediaDeviceFailure, passed straight through to LiveKit's <LiveKitRoom>. Without them, connection errors and camera/mic acquisition failures are only visible in the console:

<VideoRoom
  serverUrl={serverUrl}
  token={token}
  onError={(error) => toast.error(`Call error: ${error.message}`)}
  onMediaDeviceFailure={(failure) => toast.error(`Camera/mic error: ${failure}`)}
/>

createToken (server side) also surfaces more detail on a failed token request: if the control plane's error response has a JSON body with a string error field, the thrown message includes it — e.g. token request failed: 401 (revoked_key) — so you can log or branch on the specific failure instead of just the HTTP status.

For full control over the room, render LiveKit's <LiveKitRoom> directly (it is a direct dependency of this package) and use createToken from visuality-embed/server for the token — the control plane, not the wrapper, is the product.

Going headless

Pass children to replace the prebuilt UI, and read live state with useVideoRoom() (must be used inside a <VideoRoom>):

import { VideoRoom, useVideoRoom } from "visuality-embed";

function Roster() {
  const { participants, isConnected } = useVideoRoom();
  return <div>{isConnected ? `${participants.length} on the call` : "connecting…"}</div>;
}

<VideoRoom serverUrl={serverUrl} token={token}>
  <Roster />
</VideoRoom>;

For a real custom UI you'll want per-participant media — tracks, camera/mic state, active speaker — without importing LiveKit yourself. useParticipantMedia(identity) gives you that, and <ParticipantVideo track={...}> renders a track anywhere in your tree (an avatar, a portaled tile) with no provider ancestor required:

import { VideoRoom, useVideoRoom, useParticipantMedia, ParticipantVideo } from "visuality-embed";

function FaceTile({ identity }: { identity: string }) {
  const { cameraTrack, isMicrophoneEnabled, isSpeaking } = useParticipantMedia(identity);
  return (
    <div className={isSpeaking ? "tile speaking" : "tile"}>
      {/* 48px avatar, a portal, wherever — ParticipantVideo needs no LiveKit context */}
      <ParticipantVideo track={cameraTrack} muted style={{ width: 48, height: 48, borderRadius: "50%" }} />
      {isMicrophoneEnabled ? null : <span aria-label="muted">🔇</span>}
    </div>
  );
}

function Grid() {
  const { participants } = useVideoRoom();
  return <>{participants.map((p) => <FaceTile key={p.identity} identity={p.identity} />)}</>;
}

Audio. A headless tree renders no <VideoConference>, so nothing plays remote audio until you add it. Drop <RoomAudio /> in once (hidden <audio> elements for every remote participant), and use useAudioPlayback for the browser autoplay block — audio can't start before a user gesture:

import { RoomAudio, useAudioPlayback } from "visuality-embed";

function AudioUnlock() {
  const { canPlayAudio, startAudio } = useAudioPlayback();
  if (canPlayAudio) return null;
  return (
    <button type="button" onClick={() => void startAudio()}>
      Tap for sound
    </button>
  );
}

<VideoRoom serverUrl={serverUrl} token={token}>
  <RoomAudio />
  <AudioUnlock />
  <Grid />
</VideoRoom>;

Local device controls (join muted, build your own mic/camera buttons):

import { useLocalDevices } from "visuality-embed";

function MicButton() {
  const { isMicrophoneEnabled, setMicrophoneEnabled } = useLocalDevices();
  return (
    <button type="button" onClick={() => void setMicrophoneEnabled(!isMicrophoneEnabled)}>
      {isMicrophoneEnabled ? "Mute" : "Unmute"}
    </button>
  );
}

API

| Export | Kind | Notes | |---|---|---| | VideoRoom / VideoRoomProps | client component (visuality-embed) | serverUrl, token, optional onLeave, optional theme (defaults to the bundled "default" LiveKit theme), optional audio/video (both default true; set false to join with mic/camera off), optional onError/onMediaDeviceFailure (passed through to <LiveKitRoom>), optional children (headless). | | useVideoRoom() | hook (visuality-embed) | { room, participants, isConnected }. Inside a <VideoRoom> context only. | | useParticipantMedia(identity) / ParticipantMedia | hook (visuality-embed) | { participant, cameraTrack, microphoneTrack, screenShareTrack, isCameraEnabled, isMicrophoneEnabled, isSpeaking }, updated on room events (shallow-compared — no re-render churn). Inside a <VideoRoom> context only. | | useLocalDevices() / LocalDevices | hook (visuality-embed) | { isCameraEnabled, isMicrophoneEnabled, setCameraEnabled(on), setMicrophoneEnabled(on) } for the local participant. Inside a <VideoRoom> context only. | | ParticipantVideo / ParticipantVideoProps | client component (visuality-embed) | Renders one video track: track (a Track or TrackPublication; null renders an empty element), optional muted/className/style. Context-free — works anywhere in the tree, no LiveKit provider needed. | | RoomAudio | client component (visuality-embed) | Plays every remote participant's audio (hidden <audio> elements) — required in a headless tree, since nothing else plays sound. Inside a <VideoRoom> context only. | | useAudioPlayback() / AudioPlayback | hook (visuality-embed) | { canPlayAudio, startAudio() } — autoplay-blocked recovery; call startAudio from a user gesture. Inside a <VideoRoom> context only. | | createToken / CreateTokenInput / TokenResult | server-only fn (visuality-embed/server) | Returns { token, serverUrl }. Throws in a browser. On a non-2xx response, best-effort includes the server's error code in the thrown message. fetchImpl is injectable for tests. |

Security

  • Never expose the secret key to the client — no NEXT_PUBLIC_/VITE_ prefix, no bundling it into client code. createToken throws if it detects a browser (window), but that runtime guard is only a backstop — it can't catch every bundler/edge runtime. The reliable guarantee is to keep createToken in a server-only module: add import "server-only"; at the top of the file that calls it (Next.js), so a build fails loudly if that module is ever pulled into a client bundle. Treat the secret key as server env only.
  • The publishable key (x-vt-key) is safe in the browser; it identifies the project, it doesn't authorize minting.

Versioning & compatibility

visuality-embed and visuality-mcp are versioned in lockstep for now: the MCP scaffolds code that imports this package (the <VideoRoom> page + visuality-embed/server token route), so a given MCP release expects the matching embed release. Install the same version of both (e.g. [email protected] with [email protected]); a mismatch may scaffold imports this package doesn't expose. See CHANGELOG.md for what changed.

Releasing

This package is versioned deliberately (not on every merge to main) so consumers' package.json don't move under them. Cut a release with a v* tag:

pnpm --filter visuality-embed version patch   # or minor / major
git push --follow-tags                  # fires .github/workflows/release.yml → npm (provenance)

The package is unscoped (public by default) and builds dist/ (ESM + types) via pnpm --filter visuality-embed build — the release workflow runs it before publishing. Full flow in docs/release-process.md.