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

@packiko/video-sdk

v0.4.0

Published

Browser SDK for the Packiko video service: direct-to-Blob upload, camera capture, and playback URL resolution. Framework-agnostic core with an optional React hook.

Readme

@packiko/video-sdk

Browser SDK for the Packiko video service: direct-to-Blob upload, camera capture, and playback URL resolution. Framework-agnostic core with an optional React hook.

Published to public npm (MIT) from 0.4.0. No registry setup and no token — see docs/sdk-consumer-setup.md.

Install

pnpm add @packiko/video-sdk

Usage

Record evidence with createDurableRecorder. It commits the clip to local storage before it reports success, so leaving the screen, going offline, or reloading after stop() does not lose it — the upload continues on its own.

import { createUploadQueue, createDurableRecorder } from '@packiko/video-sdk';

// Once per application, not once per screen — it outlives the capture UI.
const queue = createUploadQueue({ apiBaseUrl, publicKey }, { attach });

const recorder = createDurableRecorder({
  queue,
  getContext: () => ({ orderRef: currentOrder.ref, context: { doc: currentOrder.id } }),
});

const session = await recorder.start();
videoEl.srcObject = session.previewStream;

await session.stop();   // durable here; no network needed
recorder.dispose();     // safe from a route cleanup, and safe to call twice

| Event | Outcome | | --- | --- | | Leaving the screen mid-recording | Rescued — dispose() finalizes rather than dropping | | Offline at stop() | Durable anyway; the upload waits for the network | | Reload once stop() resolved | Safe; the queue resumes it on the next load | | Reload or tab close mid-recording | Best effort — the browser may kill the page first | | Power loss mid-recording | Lost; no finalized clip has ever existed |

Recordings are video only. audio is off in the durable recorder's default profile, to keep clips inside their size budget and to avoid capturing workplace conversation. Pass capture: { audio: true } if a deployment genuinely needs it.

Driving capture yourself

The lower-level pieces stay available for hosts that manage their own lifecycle. Note that nothing below survives the screen unmounting mid-recording:

const recorder = createRecorder({ apiBaseUrl, publicKey });
const capture = await recorder.capture({
  videoBitsPerSecond: 1_500_000,                    // optional; omit for browser default
});                                                  // getUserMedia → CaptureHandle
capture.start();
const blob = await capture.stop();                  // finalized Blob
await recorder.upload(blob, { orderRef }).promise;  // token → PUT → confirm
// optional item-details snapshot — labels the video for AI training
await recorder.upload(blob, {
  orderRef,
  items: [
    { sku: 'SKU-1', name: 'เสื้อยืดสีแดง', qty: 2, image_url: 'https://cdn.example.com/p/1.jpg', weight_g: 350 },
  ],
}).promise;

Items are validated client-side before any byte is uploaded, with the same rules and error codes as the server (items_invalid / duplicate_item_sku). Item fields are product data only — no personal data; image_url must point at a product image, never a photo that may capture customer data (shipping labels, addresses); the service never fetches image_url (permanent SSRF constraint).

// React (peer dep ^18 || ^19) — separate sub-export
import { useRecorder } from '@packiko/video-sdk/react';
<!-- zero-build <script> tag: global `PackikoVideo` (core only, no React) -->
<script src="https://.../@packiko/video-sdk/dist/index.global.js"></script>

Durable upload queue

createUploadQueue keeps a recorded clip safe from the moment enqueue() resolves: the clip is persisted locally before any network request, survives refresh, crash, and offline periods, then uploads and attaches in the background with automatic retries. Create one queue instance per app — jobs keep settling after the screen that enqueued them is gone.

import { createUploadQueue } from '@packiko/video-sdk';

type Ctx = { documentId?: string; orderRef: string };

const queue = createUploadQueue<Ctx>(
  { apiBaseUrl, publicKey, userToken },
  {
    // Is YOUR document ready to accept the clip? false = the job parks and
    // waits (not a failure) until you nudge() or the queue re-checks.
    releaseWhen: (job) => documentExists(job.context.documentId),

    // Bind the uploaded clip to your document. MUST be idempotent — the queue
    // may call again for a clip that is already bound (e.g. after a crash);
    // a repeat call must return 'ok'.
    attach: async (job) => {
      const res = await bindClip(job.context.documentId, job.videoId);
      if (res.ok) return { status: 'ok', refId: res.bindingId };
      if (res.docNotReady) return { status: 'parked' };      // gate re-arms
      if (res.transient) return { status: 'retry', code: res.code };
      return { status: 'failed', code: res.code };           // needs a human
    },

    // Multi-user apps: whose clip is this? See "Multi-user apps" below.
    getOwnerId: () => session.userId ?? null,
  },
);

const job = await queue.enqueue({ blob, context, orderRef });
// durable from here — navigating away or losing connectivity cannot lose the clip

// the moment your document becomes ready, bind in the same cycle:
await queue.nudge((job) => job.context.documentId === createdDocumentId);

enqueue() rejects only when persisting the clip locally fails (code storage_quota_exceeded / storage_write_failed) — no network is touched before it resolves. context is yours: the SDK stores it verbatim (it must be structured-cloneable) and hands it back in every callback and outcome.

Both callbacks are optional. Upload-only integrations omit attach (and releaseWhen): a job then completes as soon as the upload is confirmed, emitting 'bound' without a refId.

Watching the queue

getSnapshot() / subscribe() drive a status tray (the snapshot reference is stable between changes — useSyncExternalStore-ready). Each UploadJob carries its stage:

| stage | meaning | |---|---| | queued → uploading → uploaded → attaching | the happy path; the job disappears once upload and attach both succeed (upload-only: once the upload is confirmed) | | retry | transient failure or an unmet releaseWhen gate — the queue retries on its own schedule (nextRetryAt is display-only) | | review | needs a human: show it, offer retry(jobId) / download(jobId) |

onOutcome() fires terminal events for toasts — 'bound', 'bind-failed' (your attach code, echoed verbatim), 'upload-failed' (an SDK PackikoErrorCode; pair with describeError for user-facing copy). hydrated is false until persisted jobs have been read back on startup — render a neutral state, not "empty". A non-null storageError means local persistence itself is failing: surface it and offer download.

import { describeError } from '@packiko/video-sdk';

const unsubscribe = queue.subscribe(() => {
  const { hydrated, jobs, storageError } = queue.getSnapshot();
  renderTray({ hydrated, jobs, storageError }); // re-render on every change
});

const offOutcome = queue.onOutcome((outcome) => {
  if (outcome.status === 'bound') toast.success(`clip attached (${outcome.videoId})`);
  else if (outcome.status === 'bind-failed') toast.error(myCopyFor(outcome.code));
  else toast.error(describeError(outcome.code, 'th')); // upload-failed
});

// tray row actions:
await queue.retry(job.id);    // re-arm a 'review'/'retry' job now
queue.download(job.id);       // hand the local clip to the user as a file

Human actions: retry(jobId) re-arms a review/retry job immediately (a file that already uploaded is never uploaded again); download(jobId) hands the locally stored clip to the user as a file. drain() asks the queue to make progress now — normally unnecessary (it wakes itself on enqueue, online, focus, and its retry schedule); call it from e.g. a service-worker message handler.

Multi-user apps

Pass getOwnerId and the queue captures who was signed in at enqueue. If a different user is signed in when the job is processed, the job moves to review with code owner_mismatch — no request is ever sent under the wrong account. When the original user signs back in, retry(jobId) resumes it. Omit getOwnerId in single-user apps.

Fast screens: freeze the binding at recording start

If one screen mounts a single recorder and swaps props to the next order while the camera is still flushing, reading live props at enqueue can bind the clip to the wrong order. Freeze the binding when recording starts and resolve it at enqueue with resolveClipBinding (pure, framework-agnostic):

import { resolveClipBinding, type ClipBinding } from '@packiko/video-sdk';

const frozen: ClipBinding = { ref: orderRef, documentId }; // captured at start()
// ... recording finishes; the screen may already show the next order ...
const bound = resolveClipBinding(frozen, { ref: liveRef, documentId: liveDocumentId });
await queue.enqueue({ blob, context: bound, orderRef: bound.ref });

Frozen values always win. One exception: a document created during/after recording — its late-arriving documentId is accepted only while the live ref still equals the frozen one.

React

import { useUploadQueue } from '@packiko/video-sdk/react';

function UploadTray({ queue }: { queue: UploadQueue<Ctx> }) {
  const { hydrated, jobs, storageError, retry, nudge, download } = useUploadQueue(queue);
  if (!hydrated) return null;
  return <>{jobs.map((j) => <Row key={j.id} job={j} onRetry={() => retry(j.id)} />)}</>;
}

Headless (no components, no CSS), SSR-safe, and React stays an optional peer dependency — non-React apps import only the core entry.

Background Sync (optional)

createUploadQueue already retries on its own foreground triggers (enqueue, online, focus, retry timer) and requests durable storage on enqueue. On Chromium browsers you can additionally let the browser wake your service worker when connectivity returns — even with the tab backgrounded. Safari/Firefox have no Background Sync: the helper resolves false silently there and the queue behaves exactly as without it.

The SDK does not ship a service worker — the recipe below goes in your app's own SW.

// page: register a one-shot sync after each enqueue (best-effort, never throws)
import { createUploadQueue, registerBackgroundSync, UPLOAD_SYNC_TAG } from '@packiko/video-sdk';

const queue = createUploadQueue(config, options);
await queue.enqueue({ blob, context, orderRef });
void registerBackgroundSync(); // or pass your own ServiceWorkerRegistration

// page: drain when the service worker asks
navigator.serviceWorker.addEventListener('message', (event) => {
  if (event.data?.type === 'packiko-video:drain') void queue.drain();
});
// sw.js (your app's service worker)
self.addEventListener('sync', (event) => {
  if (event.tag !== 'packiko-video-sdk:drain') return; // UPLOAD_SYNC_TAG
  event.waitUntil(
    self.clients
      // includeUncontrolled: a freshly loaded page is not yet controlled by
      // the SW (until the next navigation) but must still get the message.
      .matchAll({ type: 'window', includeUncontrolled: true })
      .then((clients) => clients.forEach((c) => c.postMessage({ type: 'packiko-video:drain' }))),
  );
});

With no open page the message goes nowhere — the queue then resumes on the next visit (it hydrates and drains on construction). Sync registrations are one-shot, so re-register after each enqueue; re-registering a still-pending tag is a cheap no-op.